@matter/nodejs-ble 0.16.11-alpha.0-20260228-3ee9c97df → 0.16.11

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.
@@ -3,17 +3,30 @@
3
3
  * Copyright 2022-2026 Matter.js Authors
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
- import { BleScanner as BaseBleScanner, DiscoveredBleDevice } from "#protocol";
6
+ import { ChannelType, Duration } from "#general";
7
+ import { CommissionableDevice, CommissionableDeviceIdentifiers, Scanner } from "#protocol";
7
8
  import type { Peripheral } from "@stoprocent/noble";
8
9
  import { NobleBleClient } from "./NobleBleClient.js";
9
- export type { DiscoveredBleDevice } from "#protocol";
10
- export type NobleDiscoveredBleDevice = Omit<DiscoveredBleDevice, "peripheral"> & {
10
+ export type DiscoveredBleDevice = {
11
+ deviceData: CommissionableDeviceData;
11
12
  peripheral: Peripheral;
13
+ hasAdditionalAdvertisementData: boolean;
12
14
  };
13
- export declare class BleScanner extends BaseBleScanner {
15
+ type CommissionableDeviceData = CommissionableDevice & {
16
+ SD: number;
17
+ };
18
+ export declare class BleScanner implements Scanner {
14
19
  #private;
20
+ readonly type = ChannelType.BLE;
15
21
  constructor(nobleClient: NobleBleClient);
16
- getDiscoveredDevice(address: string): NobleDiscoveredBleDevice;
17
- protected closeClient(): void;
22
+ getDiscoveredDevice(address: string): DiscoveredBleDevice;
23
+ cancelCommissionableDeviceDiscovery(identifier: CommissionableDeviceIdentifiers, resolvePromise?: boolean): void;
24
+ findOperationalDevice(): Promise<undefined>;
25
+ getDiscoveredOperationalDevice(): undefined;
26
+ findCommissionableDevices(identifier: CommissionableDeviceIdentifiers, timeout?: Duration, ignoreExistingRecords?: boolean): Promise<CommissionableDevice[]>;
27
+ findCommissionableDevicesContinuously(identifier: CommissionableDeviceIdentifiers, callback: (device: CommissionableDevice) => void, timeout?: Duration, cancelSignal?: Promise<void>): Promise<CommissionableDevice[]>;
28
+ getDiscoveredCommissionableDevices(identifier: CommissionableDeviceIdentifiers): CommissionableDevice[];
29
+ close(): Promise<void>;
18
30
  }
31
+ export {};
19
32
  //# sourceMappingURL=BleScanner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"BleScanner.d.ts","sourceRoot":"","sources":["../../src/BleScanner.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,UAAU,IAAI,cAAc,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAC9E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,YAAY,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAErD,MAAM,MAAM,wBAAwB,GAAG,IAAI,CAAC,mBAAmB,EAAE,YAAY,CAAC,GAAG;IAAE,UAAU,EAAE,UAAU,CAAA;CAAE,CAAC;AAE5G,qBAAa,UAAW,SAAQ,cAAc;;gBAG9B,WAAW,EAAE,cAAc;IAK9B,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,wBAAwB;cAIpD,WAAW;CAGjC"}
1
+ {"version":3,"file":"BleScanner.d.ts","sourceRoot":"","sources":["../../src/BleScanner.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAEH,WAAW,EAGX,QAAQ,EAOX,MAAM,UAAU,CAAC;AAClB,OAAO,EAAsB,oBAAoB,EAAE,+BAA+B,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE/G,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAIrD,MAAM,MAAM,mBAAmB,GAAG;IAC9B,UAAU,EAAE,wBAAwB,CAAC;IACrC,UAAU,EAAE,UAAU,CAAC;IACvB,8BAA8B,EAAE,OAAO,CAAC;CAC3C,CAAC;AAEF,KAAK,wBAAwB,GAAG,oBAAoB,GAAG;IACnD,EAAE,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,qBAAa,UAAW,YAAW,OAAO;;IACtC,QAAQ,CAAC,IAAI,mBAAmB;gBAcpB,WAAW,EAAE,cAAc;IAOhC,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB;IAoDhE,mCAAmC,CAAC,UAAU,EAAE,+BAA+B,EAAE,cAAc,UAAO;IA+HhG,qBAAqB,IAAI,OAAO,CAAC,SAAS,CAAC;IAKjD,8BAA8B,IAAI,SAAS;IAKrC,yBAAyB,CAC3B,UAAU,EAAE,+BAA+B,EAC3C,OAAO,WAAc,EACrB,qBAAqB,UAAQ,GAC9B,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAqB5B,qCAAqC,CACvC,UAAU,EAAE,+BAA+B,EAC3C,QAAQ,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,EAChD,OAAO,CAAC,EAAE,QAAQ,EAClB,YAAY,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,GAC7B,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAgDlC,kCAAkC,CAAC,UAAU,EAAE,+BAA+B,GAAG,oBAAoB,EAAE;IAIjG,KAAK;CAMd"}
@@ -21,23 +21,250 @@ __export(BleScanner_exports, {
21
21
  BleScanner: () => BleScanner
22
22
  });
23
23
  module.exports = __toCommonJS(BleScanner_exports);
24
+ var import_general = require("#general");
24
25
  var import_protocol = require("#protocol");
26
+ var import_types = require("#types");
25
27
  /**
26
28
  * @license
27
29
  * Copyright 2022-2026 Matter.js Authors
28
30
  * SPDX-License-Identifier: Apache-2.0
29
31
  */
30
- class BleScanner extends import_protocol.BleScanner {
32
+ const logger = import_general.Logger.get("BleScanner");
33
+ class BleScanner {
34
+ type = import_general.ChannelType.BLE;
31
35
  #nobleClient;
36
+ #recordWaiters = /* @__PURE__ */ new Map();
37
+ #discoveredMatterDevices = /* @__PURE__ */ new Map();
32
38
  constructor(nobleClient) {
33
- super(nobleClient);
34
39
  this.#nobleClient = nobleClient;
40
+ this.#nobleClient.setDiscoveryCallback(
41
+ (address, manufacturerData) => this.#handleDiscoveredDevice(address, manufacturerData)
42
+ );
35
43
  }
36
44
  getDiscoveredDevice(address) {
37
- return super.getDiscoveredDevice(address);
45
+ const device = this.#discoveredMatterDevices.get(address);
46
+ if (device === void 0) {
47
+ throw new import_protocol.BleError(`No device found for address ${address}`);
48
+ }
49
+ return device;
38
50
  }
39
- closeClient() {
51
+ /**
52
+ * Registers a deferred promise for a specific queryId together with a timeout and return the promise.
53
+ * The promise will be resolved when the timer runs out latest.
54
+ */
55
+ async #registerWaiterPromise(queryId, timeout, resolveOnUpdatedRecords = true, cancelResolver) {
56
+ const { promise, resolver } = (0, import_general.createPromise)();
57
+ let timer;
58
+ if (timeout) {
59
+ timer = import_general.Time.getTimer("BLE query timeout", timeout, () => {
60
+ cancelResolver?.();
61
+ this.#finishWaiter(queryId, true);
62
+ }).start();
63
+ }
64
+ this.#recordWaiters.set(queryId, { resolver, timer, resolveOnUpdatedRecords, cancelResolver });
65
+ logger.debug(
66
+ `Registered waiter for query ${queryId} with timeout ${timeout === void 0 ? "(none)" : import_general.Duration.format(timeout)} ${resolveOnUpdatedRecords ? "" : " (not resolving on updated records)"}`
67
+ );
68
+ await promise;
69
+ }
70
+ /**
71
+ * Remove a waiter promise for a specific queryId and stop the connected timer. If required also resolve the
72
+ * promise.
73
+ */
74
+ #finishWaiter(queryId, resolvePromise, isUpdatedRecord = false) {
75
+ const waiter = this.#recordWaiters.get(queryId);
76
+ if (waiter === void 0) return;
77
+ const { timer, resolver, resolveOnUpdatedRecords } = waiter;
78
+ if (isUpdatedRecord && !resolveOnUpdatedRecords) return;
79
+ logger.debug(`Finishing waiter for query ${queryId}, resolving: ${resolvePromise}`);
80
+ timer?.stop();
81
+ if (resolvePromise) {
82
+ resolver();
83
+ }
84
+ this.#recordWaiters.delete(queryId);
85
+ }
86
+ cancelCommissionableDeviceDiscovery(identifier, resolvePromise = true) {
87
+ const queryKey = this.#buildCommissionableQueryIdentifier(identifier);
88
+ const { cancelResolver } = this.#recordWaiters.get(queryKey) ?? {};
89
+ cancelResolver?.();
90
+ this.#finishWaiter(queryKey, resolvePromise);
91
+ }
92
+ #handleDiscoveredDevice(peripheral, manufacturerServiceData) {
93
+ const address = peripheral.address;
94
+ try {
95
+ const { discriminator, vendorId, productId, hasAdditionalAdvertisementData } = import_protocol.BtpCodec.decodeBleAdvertisementServiceData(manufacturerServiceData);
96
+ const deviceData = {
97
+ deviceIdentifier: address,
98
+ D: discriminator,
99
+ SD: discriminator >> 8 & 15,
100
+ VP: `${vendorId}+${productId}`,
101
+ CM: 1,
102
+ // Can be no other mode,
103
+ addresses: [{ type: "ble", peripheralAddress: address }]
104
+ };
105
+ const deviceExisting = this.#discoveredMatterDevices.has(address);
106
+ logger.debug(
107
+ `${deviceExisting ? "Re-" : ""}Discovered device ${address} data: ${import_general.Diagnostic.json(deviceData)}`
108
+ );
109
+ this.#discoveredMatterDevices.set(address, {
110
+ deviceData,
111
+ peripheral,
112
+ hasAdditionalAdvertisementData
113
+ });
114
+ const queryKey = this.#findCommissionableQueryIdentifier(deviceData);
115
+ if (queryKey !== void 0) {
116
+ this.#finishWaiter(queryKey, true, deviceExisting);
117
+ }
118
+ } catch (error) {
119
+ logger.debug(
120
+ `Discovered device ${address} ${manufacturerServiceData === void 0 ? void 0 : import_general.Bytes.toHex(manufacturerServiceData)} does not seem to be a valid Matter device: ${error}`
121
+ );
122
+ }
123
+ }
124
+ #findCommissionableQueryIdentifier(record) {
125
+ const longDiscriminatorQueryId = this.#buildCommissionableQueryIdentifier({ longDiscriminator: record.D });
126
+ if (this.#recordWaiters.has(longDiscriminatorQueryId)) {
127
+ return longDiscriminatorQueryId;
128
+ }
129
+ const shortDiscriminatorQueryId = this.#buildCommissionableQueryIdentifier({ shortDiscriminator: record.SD });
130
+ if (this.#recordWaiters.has(shortDiscriminatorQueryId)) {
131
+ return shortDiscriminatorQueryId;
132
+ }
133
+ if (record.VP !== void 0) {
134
+ const vpParts = record.VP.split("+");
135
+ const vendorIdQueryId = this.#buildCommissionableQueryIdentifier({
136
+ vendorId: (0, import_types.VendorId)(parseInt(vpParts[0]))
137
+ });
138
+ if (this.#recordWaiters.has(vendorIdQueryId)) {
139
+ return vendorIdQueryId;
140
+ }
141
+ if (vpParts[1] !== void 0) {
142
+ const productIdQueryId = this.#buildCommissionableQueryIdentifier({
143
+ productId: parseInt(vpParts[1])
144
+ });
145
+ if (this.#recordWaiters.has(productIdQueryId)) {
146
+ return productIdQueryId;
147
+ }
148
+ }
149
+ }
150
+ if (this.#recordWaiters.has("*")) {
151
+ return "*";
152
+ }
153
+ return void 0;
154
+ }
155
+ /**
156
+ * Builds an identifier string for commissionable queries based on the given identifier object.
157
+ * Some identifiers are identical to the official DNS-SD identifiers, others are custom.
158
+ */
159
+ #buildCommissionableQueryIdentifier(identifier) {
160
+ if ("longDiscriminator" in identifier) {
161
+ return `D:${identifier.longDiscriminator}`;
162
+ } else if ("shortDiscriminator" in identifier) {
163
+ return `SD:${identifier.shortDiscriminator}`;
164
+ } else if ("vendorId" in identifier) {
165
+ return `V:${identifier.vendorId}`;
166
+ } else if ("productId" in identifier) {
167
+ return `P:${identifier.productId}`;
168
+ } else return "*";
169
+ }
170
+ #getCommissionableDevices(identifier) {
171
+ const storedRecords = Array.from(this.#discoveredMatterDevices.values());
172
+ const foundRecords = new Array();
173
+ if ("longDiscriminator" in identifier) {
174
+ foundRecords.push(...storedRecords.filter(({ deviceData: { D } }) => D === identifier.longDiscriminator));
175
+ } else if ("shortDiscriminator" in identifier) {
176
+ foundRecords.push(
177
+ ...storedRecords.filter(({ deviceData: { SD } }) => SD === identifier.shortDiscriminator)
178
+ );
179
+ } else if ("vendorId" in identifier) {
180
+ foundRecords.push(
181
+ ...storedRecords.filter(
182
+ ({ deviceData: { VP } }) => VP === `${identifier.vendorId}` || VP?.startsWith(`${identifier.vendorId}+`)
183
+ )
184
+ );
185
+ } else if ("productId" in identifier) {
186
+ foundRecords.push(
187
+ ...storedRecords.filter(({ deviceData: { VP } }) => VP?.endsWith(`+${identifier.productId}`))
188
+ );
189
+ } else {
190
+ foundRecords.push(...storedRecords.filter(({ deviceData: { CM } }) => CM === 1 || CM === 2));
191
+ }
192
+ return foundRecords;
193
+ }
194
+ async findOperationalDevice() {
195
+ logger.info(`skip BLE scan because scanning for operational devices is not supported`);
196
+ return void 0;
197
+ }
198
+ getDiscoveredOperationalDevice() {
199
+ logger.info(`skip BLE scan because scanning for operational devices is not supported`);
200
+ return void 0;
201
+ }
202
+ async findCommissionableDevices(identifier, timeout = (0, import_general.Seconds)(10), ignoreExistingRecords = false) {
203
+ let storedRecords = this.#getCommissionableDevices(identifier);
204
+ if (ignoreExistingRecords) {
205
+ for (const record of storedRecords) {
206
+ this.#discoveredMatterDevices.delete(record.peripheral.address);
207
+ }
208
+ storedRecords = [];
209
+ }
210
+ if (storedRecords.length === 0) {
211
+ const queryKey = this.#buildCommissionableQueryIdentifier(identifier);
212
+ await this.#nobleClient.startScanning();
213
+ await this.#registerWaiterPromise(queryKey, timeout);
214
+ storedRecords = this.#getCommissionableDevices(identifier);
215
+ await this.#nobleClient.stopScanning();
216
+ }
217
+ return storedRecords.map(({ deviceData }) => deviceData);
218
+ }
219
+ async findCommissionableDevicesContinuously(identifier, callback, timeout, cancelSignal) {
220
+ const discoveredDevices = /* @__PURE__ */ new Set();
221
+ const discoveryEndTime = timeout ? import_general.Time.nowMs + timeout : void 0;
222
+ const queryKey = this.#buildCommissionableQueryIdentifier(identifier);
223
+ await this.#nobleClient.startScanning();
224
+ let queryResolver;
225
+ if (cancelSignal === void 0) {
226
+ const { promise, resolver } = (0, import_general.createPromise)();
227
+ cancelSignal = promise;
228
+ queryResolver = resolver;
229
+ }
230
+ let canceled = false;
231
+ cancelSignal?.then(
232
+ () => {
233
+ canceled = true;
234
+ this.#finishWaiter(queryKey, true);
235
+ },
236
+ (cause) => {
237
+ logger.error("Unexpected error canceling commissioning", cause);
238
+ }
239
+ );
240
+ while (!canceled) {
241
+ this.#getCommissionableDevices(identifier).forEach(({ deviceData }) => {
242
+ const { deviceIdentifier } = deviceData;
243
+ if (!discoveredDevices.has(deviceIdentifier)) {
244
+ discoveredDevices.add(deviceIdentifier);
245
+ callback(deviceData);
246
+ }
247
+ });
248
+ let remainingTime;
249
+ if (discoveryEndTime !== void 0) {
250
+ remainingTime = import_general.Millis.ceil((0, import_general.Timespan)(import_general.Time.nowMs, discoveryEndTime).duration);
251
+ if (remainingTime <= 0) {
252
+ break;
253
+ }
254
+ }
255
+ await this.#registerWaiterPromise(queryKey, remainingTime, false, queryResolver);
256
+ }
257
+ await this.#nobleClient.stopScanning();
258
+ return this.#getCommissionableDevices(identifier).map(({ deviceData }) => deviceData);
259
+ }
260
+ getDiscoveredCommissionableDevices(identifier) {
261
+ return this.#getCommissionableDevices(identifier).map(({ deviceData }) => deviceData);
262
+ }
263
+ async close() {
40
264
  this.#nobleClient.close();
265
+ [...this.#recordWaiters.keys()].forEach(
266
+ (queryId) => this.#finishWaiter(queryId, !!this.#recordWaiters.get(queryId)?.timer)
267
+ );
41
268
  }
42
269
  }
43
270
  //# sourceMappingURL=BleScanner.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/BleScanner.ts"],
4
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAMA,sBAAkE;AANlE;AAAA;AAAA;AAAA;AAAA;AAcO,MAAM,mBAAmB,gBAAAA,WAAe;AAAA,EAClC;AAAA,EAET,YAAY,aAA6B;AACrC,UAAM,WAAW;AACjB,SAAK,eAAe;AAAA,EACxB;AAAA,EAES,oBAAoB,SAA2C;AACpE,WAAO,MAAM,oBAAoB,OAAO;AAAA,EAC5C;AAAA,EAEmB,cAAc;AAC7B,SAAK,aAAa,MAAM;AAAA,EAC5B;AACJ;",
5
- "names": ["BaseBleScanner"]
4
+ "mappings": "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",
5
+ "names": []
6
6
  }
@@ -3,17 +3,30 @@
3
3
  * Copyright 2022-2026 Matter.js Authors
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
- import { BleScanner as BaseBleScanner, DiscoveredBleDevice } from "#protocol";
6
+ import { ChannelType, Duration } from "#general";
7
+ import { CommissionableDevice, CommissionableDeviceIdentifiers, Scanner } from "#protocol";
7
8
  import type { Peripheral } from "@stoprocent/noble";
8
9
  import { NobleBleClient } from "./NobleBleClient.js";
9
- export type { DiscoveredBleDevice } from "#protocol";
10
- export type NobleDiscoveredBleDevice = Omit<DiscoveredBleDevice, "peripheral"> & {
10
+ export type DiscoveredBleDevice = {
11
+ deviceData: CommissionableDeviceData;
11
12
  peripheral: Peripheral;
13
+ hasAdditionalAdvertisementData: boolean;
12
14
  };
13
- export declare class BleScanner extends BaseBleScanner {
15
+ type CommissionableDeviceData = CommissionableDevice & {
16
+ SD: number;
17
+ };
18
+ export declare class BleScanner implements Scanner {
14
19
  #private;
20
+ readonly type = ChannelType.BLE;
15
21
  constructor(nobleClient: NobleBleClient);
16
- getDiscoveredDevice(address: string): NobleDiscoveredBleDevice;
17
- protected closeClient(): void;
22
+ getDiscoveredDevice(address: string): DiscoveredBleDevice;
23
+ cancelCommissionableDeviceDiscovery(identifier: CommissionableDeviceIdentifiers, resolvePromise?: boolean): void;
24
+ findOperationalDevice(): Promise<undefined>;
25
+ getDiscoveredOperationalDevice(): undefined;
26
+ findCommissionableDevices(identifier: CommissionableDeviceIdentifiers, timeout?: Duration, ignoreExistingRecords?: boolean): Promise<CommissionableDevice[]>;
27
+ findCommissionableDevicesContinuously(identifier: CommissionableDeviceIdentifiers, callback: (device: CommissionableDevice) => void, timeout?: Duration, cancelSignal?: Promise<void>): Promise<CommissionableDevice[]>;
28
+ getDiscoveredCommissionableDevices(identifier: CommissionableDeviceIdentifiers): CommissionableDevice[];
29
+ close(): Promise<void>;
18
30
  }
31
+ export {};
19
32
  //# sourceMappingURL=BleScanner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"BleScanner.d.ts","sourceRoot":"","sources":["../../src/BleScanner.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,UAAU,IAAI,cAAc,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAC9E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,YAAY,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAErD,MAAM,MAAM,wBAAwB,GAAG,IAAI,CAAC,mBAAmB,EAAE,YAAY,CAAC,GAAG;IAAE,UAAU,EAAE,UAAU,CAAA;CAAE,CAAC;AAE5G,qBAAa,UAAW,SAAQ,cAAc;;gBAG9B,WAAW,EAAE,cAAc;IAK9B,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,wBAAwB;cAIpD,WAAW;CAGjC"}
1
+ {"version":3,"file":"BleScanner.d.ts","sourceRoot":"","sources":["../../src/BleScanner.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAEH,WAAW,EAGX,QAAQ,EAOX,MAAM,UAAU,CAAC;AAClB,OAAO,EAAsB,oBAAoB,EAAE,+BAA+B,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE/G,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAIrD,MAAM,MAAM,mBAAmB,GAAG;IAC9B,UAAU,EAAE,wBAAwB,CAAC;IACrC,UAAU,EAAE,UAAU,CAAC;IACvB,8BAA8B,EAAE,OAAO,CAAC;CAC3C,CAAC;AAEF,KAAK,wBAAwB,GAAG,oBAAoB,GAAG;IACnD,EAAE,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,qBAAa,UAAW,YAAW,OAAO;;IACtC,QAAQ,CAAC,IAAI,mBAAmB;gBAcpB,WAAW,EAAE,cAAc;IAOhC,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB;IAoDhE,mCAAmC,CAAC,UAAU,EAAE,+BAA+B,EAAE,cAAc,UAAO;IA+HhG,qBAAqB,IAAI,OAAO,CAAC,SAAS,CAAC;IAKjD,8BAA8B,IAAI,SAAS;IAKrC,yBAAyB,CAC3B,UAAU,EAAE,+BAA+B,EAC3C,OAAO,WAAc,EACrB,qBAAqB,UAAQ,GAC9B,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAqB5B,qCAAqC,CACvC,UAAU,EAAE,+BAA+B,EAC3C,QAAQ,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,EAChD,OAAO,CAAC,EAAE,QAAQ,EAClB,YAAY,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,GAC7B,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAgDlC,kCAAkC,CAAC,UAAU,EAAE,+BAA+B,GAAG,oBAAoB,EAAE;IAIjG,KAAK;CAMd"}
@@ -3,18 +3,256 @@
3
3
  * Copyright 2022-2026 Matter.js Authors
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
- import { BleScanner as BaseBleScanner } from "#protocol";
7
- class BleScanner extends BaseBleScanner {
6
+ import {
7
+ Bytes,
8
+ ChannelType,
9
+ createPromise,
10
+ Diagnostic,
11
+ Duration,
12
+ Logger,
13
+ Millis,
14
+ Seconds,
15
+ Time,
16
+ Timespan
17
+ } from "#general";
18
+ import { BleError, BtpCodec } from "#protocol";
19
+ import { VendorId } from "#types";
20
+ const logger = Logger.get("BleScanner");
21
+ class BleScanner {
22
+ type = ChannelType.BLE;
8
23
  #nobleClient;
24
+ #recordWaiters = /* @__PURE__ */ new Map();
25
+ #discoveredMatterDevices = /* @__PURE__ */ new Map();
9
26
  constructor(nobleClient) {
10
- super(nobleClient);
11
27
  this.#nobleClient = nobleClient;
28
+ this.#nobleClient.setDiscoveryCallback(
29
+ (address, manufacturerData) => this.#handleDiscoveredDevice(address, manufacturerData)
30
+ );
12
31
  }
13
32
  getDiscoveredDevice(address) {
14
- return super.getDiscoveredDevice(address);
33
+ const device = this.#discoveredMatterDevices.get(address);
34
+ if (device === void 0) {
35
+ throw new BleError(`No device found for address ${address}`);
36
+ }
37
+ return device;
15
38
  }
16
- closeClient() {
39
+ /**
40
+ * Registers a deferred promise for a specific queryId together with a timeout and return the promise.
41
+ * The promise will be resolved when the timer runs out latest.
42
+ */
43
+ async #registerWaiterPromise(queryId, timeout, resolveOnUpdatedRecords = true, cancelResolver) {
44
+ const { promise, resolver } = createPromise();
45
+ let timer;
46
+ if (timeout) {
47
+ timer = Time.getTimer("BLE query timeout", timeout, () => {
48
+ cancelResolver?.();
49
+ this.#finishWaiter(queryId, true);
50
+ }).start();
51
+ }
52
+ this.#recordWaiters.set(queryId, { resolver, timer, resolveOnUpdatedRecords, cancelResolver });
53
+ logger.debug(
54
+ `Registered waiter for query ${queryId} with timeout ${timeout === void 0 ? "(none)" : Duration.format(timeout)} ${resolveOnUpdatedRecords ? "" : " (not resolving on updated records)"}`
55
+ );
56
+ await promise;
57
+ }
58
+ /**
59
+ * Remove a waiter promise for a specific queryId and stop the connected timer. If required also resolve the
60
+ * promise.
61
+ */
62
+ #finishWaiter(queryId, resolvePromise, isUpdatedRecord = false) {
63
+ const waiter = this.#recordWaiters.get(queryId);
64
+ if (waiter === void 0) return;
65
+ const { timer, resolver, resolveOnUpdatedRecords } = waiter;
66
+ if (isUpdatedRecord && !resolveOnUpdatedRecords) return;
67
+ logger.debug(`Finishing waiter for query ${queryId}, resolving: ${resolvePromise}`);
68
+ timer?.stop();
69
+ if (resolvePromise) {
70
+ resolver();
71
+ }
72
+ this.#recordWaiters.delete(queryId);
73
+ }
74
+ cancelCommissionableDeviceDiscovery(identifier, resolvePromise = true) {
75
+ const queryKey = this.#buildCommissionableQueryIdentifier(identifier);
76
+ const { cancelResolver } = this.#recordWaiters.get(queryKey) ?? {};
77
+ cancelResolver?.();
78
+ this.#finishWaiter(queryKey, resolvePromise);
79
+ }
80
+ #handleDiscoveredDevice(peripheral, manufacturerServiceData) {
81
+ const address = peripheral.address;
82
+ try {
83
+ const { discriminator, vendorId, productId, hasAdditionalAdvertisementData } = BtpCodec.decodeBleAdvertisementServiceData(manufacturerServiceData);
84
+ const deviceData = {
85
+ deviceIdentifier: address,
86
+ D: discriminator,
87
+ SD: discriminator >> 8 & 15,
88
+ VP: `${vendorId}+${productId}`,
89
+ CM: 1,
90
+ // Can be no other mode,
91
+ addresses: [{ type: "ble", peripheralAddress: address }]
92
+ };
93
+ const deviceExisting = this.#discoveredMatterDevices.has(address);
94
+ logger.debug(
95
+ `${deviceExisting ? "Re-" : ""}Discovered device ${address} data: ${Diagnostic.json(deviceData)}`
96
+ );
97
+ this.#discoveredMatterDevices.set(address, {
98
+ deviceData,
99
+ peripheral,
100
+ hasAdditionalAdvertisementData
101
+ });
102
+ const queryKey = this.#findCommissionableQueryIdentifier(deviceData);
103
+ if (queryKey !== void 0) {
104
+ this.#finishWaiter(queryKey, true, deviceExisting);
105
+ }
106
+ } catch (error) {
107
+ logger.debug(
108
+ `Discovered device ${address} ${manufacturerServiceData === void 0 ? void 0 : Bytes.toHex(manufacturerServiceData)} does not seem to be a valid Matter device: ${error}`
109
+ );
110
+ }
111
+ }
112
+ #findCommissionableQueryIdentifier(record) {
113
+ const longDiscriminatorQueryId = this.#buildCommissionableQueryIdentifier({ longDiscriminator: record.D });
114
+ if (this.#recordWaiters.has(longDiscriminatorQueryId)) {
115
+ return longDiscriminatorQueryId;
116
+ }
117
+ const shortDiscriminatorQueryId = this.#buildCommissionableQueryIdentifier({ shortDiscriminator: record.SD });
118
+ if (this.#recordWaiters.has(shortDiscriminatorQueryId)) {
119
+ return shortDiscriminatorQueryId;
120
+ }
121
+ if (record.VP !== void 0) {
122
+ const vpParts = record.VP.split("+");
123
+ const vendorIdQueryId = this.#buildCommissionableQueryIdentifier({
124
+ vendorId: VendorId(parseInt(vpParts[0]))
125
+ });
126
+ if (this.#recordWaiters.has(vendorIdQueryId)) {
127
+ return vendorIdQueryId;
128
+ }
129
+ if (vpParts[1] !== void 0) {
130
+ const productIdQueryId = this.#buildCommissionableQueryIdentifier({
131
+ productId: parseInt(vpParts[1])
132
+ });
133
+ if (this.#recordWaiters.has(productIdQueryId)) {
134
+ return productIdQueryId;
135
+ }
136
+ }
137
+ }
138
+ if (this.#recordWaiters.has("*")) {
139
+ return "*";
140
+ }
141
+ return void 0;
142
+ }
143
+ /**
144
+ * Builds an identifier string for commissionable queries based on the given identifier object.
145
+ * Some identifiers are identical to the official DNS-SD identifiers, others are custom.
146
+ */
147
+ #buildCommissionableQueryIdentifier(identifier) {
148
+ if ("longDiscriminator" in identifier) {
149
+ return `D:${identifier.longDiscriminator}`;
150
+ } else if ("shortDiscriminator" in identifier) {
151
+ return `SD:${identifier.shortDiscriminator}`;
152
+ } else if ("vendorId" in identifier) {
153
+ return `V:${identifier.vendorId}`;
154
+ } else if ("productId" in identifier) {
155
+ return `P:${identifier.productId}`;
156
+ } else return "*";
157
+ }
158
+ #getCommissionableDevices(identifier) {
159
+ const storedRecords = Array.from(this.#discoveredMatterDevices.values());
160
+ const foundRecords = new Array();
161
+ if ("longDiscriminator" in identifier) {
162
+ foundRecords.push(...storedRecords.filter(({ deviceData: { D } }) => D === identifier.longDiscriminator));
163
+ } else if ("shortDiscriminator" in identifier) {
164
+ foundRecords.push(
165
+ ...storedRecords.filter(({ deviceData: { SD } }) => SD === identifier.shortDiscriminator)
166
+ );
167
+ } else if ("vendorId" in identifier) {
168
+ foundRecords.push(
169
+ ...storedRecords.filter(
170
+ ({ deviceData: { VP } }) => VP === `${identifier.vendorId}` || VP?.startsWith(`${identifier.vendorId}+`)
171
+ )
172
+ );
173
+ } else if ("productId" in identifier) {
174
+ foundRecords.push(
175
+ ...storedRecords.filter(({ deviceData: { VP } }) => VP?.endsWith(`+${identifier.productId}`))
176
+ );
177
+ } else {
178
+ foundRecords.push(...storedRecords.filter(({ deviceData: { CM } }) => CM === 1 || CM === 2));
179
+ }
180
+ return foundRecords;
181
+ }
182
+ async findOperationalDevice() {
183
+ logger.info(`skip BLE scan because scanning for operational devices is not supported`);
184
+ return void 0;
185
+ }
186
+ getDiscoveredOperationalDevice() {
187
+ logger.info(`skip BLE scan because scanning for operational devices is not supported`);
188
+ return void 0;
189
+ }
190
+ async findCommissionableDevices(identifier, timeout = Seconds(10), ignoreExistingRecords = false) {
191
+ let storedRecords = this.#getCommissionableDevices(identifier);
192
+ if (ignoreExistingRecords) {
193
+ for (const record of storedRecords) {
194
+ this.#discoveredMatterDevices.delete(record.peripheral.address);
195
+ }
196
+ storedRecords = [];
197
+ }
198
+ if (storedRecords.length === 0) {
199
+ const queryKey = this.#buildCommissionableQueryIdentifier(identifier);
200
+ await this.#nobleClient.startScanning();
201
+ await this.#registerWaiterPromise(queryKey, timeout);
202
+ storedRecords = this.#getCommissionableDevices(identifier);
203
+ await this.#nobleClient.stopScanning();
204
+ }
205
+ return storedRecords.map(({ deviceData }) => deviceData);
206
+ }
207
+ async findCommissionableDevicesContinuously(identifier, callback, timeout, cancelSignal) {
208
+ const discoveredDevices = /* @__PURE__ */ new Set();
209
+ const discoveryEndTime = timeout ? Time.nowMs + timeout : void 0;
210
+ const queryKey = this.#buildCommissionableQueryIdentifier(identifier);
211
+ await this.#nobleClient.startScanning();
212
+ let queryResolver;
213
+ if (cancelSignal === void 0) {
214
+ const { promise, resolver } = createPromise();
215
+ cancelSignal = promise;
216
+ queryResolver = resolver;
217
+ }
218
+ let canceled = false;
219
+ cancelSignal?.then(
220
+ () => {
221
+ canceled = true;
222
+ this.#finishWaiter(queryKey, true);
223
+ },
224
+ (cause) => {
225
+ logger.error("Unexpected error canceling commissioning", cause);
226
+ }
227
+ );
228
+ while (!canceled) {
229
+ this.#getCommissionableDevices(identifier).forEach(({ deviceData }) => {
230
+ const { deviceIdentifier } = deviceData;
231
+ if (!discoveredDevices.has(deviceIdentifier)) {
232
+ discoveredDevices.add(deviceIdentifier);
233
+ callback(deviceData);
234
+ }
235
+ });
236
+ let remainingTime;
237
+ if (discoveryEndTime !== void 0) {
238
+ remainingTime = Millis.ceil(Timespan(Time.nowMs, discoveryEndTime).duration);
239
+ if (remainingTime <= 0) {
240
+ break;
241
+ }
242
+ }
243
+ await this.#registerWaiterPromise(queryKey, remainingTime, false, queryResolver);
244
+ }
245
+ await this.#nobleClient.stopScanning();
246
+ return this.#getCommissionableDevices(identifier).map(({ deviceData }) => deviceData);
247
+ }
248
+ getDiscoveredCommissionableDevices(identifier) {
249
+ return this.#getCommissionableDevices(identifier).map(({ deviceData }) => deviceData);
250
+ }
251
+ async close() {
17
252
  this.#nobleClient.close();
253
+ [...this.#recordWaiters.keys()].forEach(
254
+ (queryId) => this.#finishWaiter(queryId, !!this.#recordWaiters.get(queryId)?.timer)
255
+ );
18
256
  }
19
257
  }
20
258
  export {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/BleScanner.ts"],
4
- "mappings": "AAAA;AAAA;AAAA;AAAA;AAAA;AAMA,SAAS,cAAc,sBAA2C;AAQ3D,MAAM,mBAAmB,eAAe;AAAA,EAClC;AAAA,EAET,YAAY,aAA6B;AACrC,UAAM,WAAW;AACjB,SAAK,eAAe;AAAA,EACxB;AAAA,EAES,oBAAoB,SAA2C;AACpE,WAAO,MAAM,oBAAoB,OAAO;AAAA,EAC5C;AAAA,EAEmB,cAAc;AAC7B,SAAK,aAAa,MAAM;AAAA,EAC5B;AACJ;",
4
+ "mappings": "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",
5
5
  "names": []
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matter/nodejs-ble",
3
- "version": "0.16.11-alpha.0-20260228-3ee9c97df",
3
+ "version": "0.16.11",
4
4
  "description": "Matter BLE support for node.js",
5
5
  "keywords": [
6
6
  "iot",
@@ -28,16 +28,16 @@
28
28
  "build-clean": "matter-build --clean"
29
29
  },
30
30
  "dependencies": {
31
- "@matter/general": "0.16.11-alpha.0-20260228-3ee9c97df",
32
- "@matter/protocol": "0.16.11-alpha.0-20260228-3ee9c97df",
33
- "@matter/types": "0.16.11-alpha.0-20260228-3ee9c97df"
31
+ "@matter/general": "0.16.11",
32
+ "@matter/protocol": "0.16.11",
33
+ "@matter/types": "0.16.11"
34
34
  },
35
35
  "devDependencies": {
36
- "@matter/tools": "0.16.11-alpha.0-20260228-3ee9c97df"
36
+ "@matter/tools": "0.16.11"
37
37
  },
38
38
  "optionalDependencies": {
39
- "@stoprocent/bleno": "^0.12.3",
40
- "@stoprocent/noble": "^2.3.16"
39
+ "@stoprocent/bleno": "^0.12.1",
40
+ "@stoprocent/noble": "^2.3.10"
41
41
  },
42
42
  "engines": {
43
43
  "node": ">=20.19.0 <22.0.0 || >=22.13.0"
package/src/BleScanner.ts CHANGED
@@ -4,27 +4,333 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { BleScanner as BaseBleScanner, DiscoveredBleDevice } from "#protocol";
7
+ import {
8
+ Bytes,
9
+ ChannelType,
10
+ createPromise,
11
+ Diagnostic,
12
+ Duration,
13
+ Logger,
14
+ Millis,
15
+ Seconds,
16
+ Time,
17
+ Timer,
18
+ Timespan,
19
+ } from "#general";
20
+ import { BleError, BtpCodec, CommissionableDevice, CommissionableDeviceIdentifiers, Scanner } from "#protocol";
21
+ import { VendorId } from "#types";
8
22
  import type { Peripheral } from "@stoprocent/noble";
9
23
  import { NobleBleClient } from "./NobleBleClient.js";
10
24
 
11
- export type { DiscoveredBleDevice } from "#protocol";
25
+ const logger = Logger.get("BleScanner");
12
26
 
13
- export type NobleDiscoveredBleDevice = Omit<DiscoveredBleDevice, "peripheral"> & { peripheral: Peripheral };
27
+ export type DiscoveredBleDevice = {
28
+ deviceData: CommissionableDeviceData;
29
+ peripheral: Peripheral;
30
+ hasAdditionalAdvertisementData: boolean;
31
+ };
32
+
33
+ type CommissionableDeviceData = CommissionableDevice & {
34
+ SD: number; // Additional Field for Short discriminator
35
+ };
36
+
37
+ export class BleScanner implements Scanner {
38
+ readonly type = ChannelType.BLE;
14
39
 
15
- export class BleScanner extends BaseBleScanner {
16
40
  readonly #nobleClient: NobleBleClient;
41
+ readonly #recordWaiters = new Map<
42
+ string,
43
+ {
44
+ resolver: () => void;
45
+ timer?: Timer;
46
+ resolveOnUpdatedRecords: boolean;
47
+ cancelResolver?: (value: void) => void;
48
+ }
49
+ >();
50
+ readonly #discoveredMatterDevices = new Map<string, DiscoveredBleDevice>();
17
51
 
18
52
  constructor(nobleClient: NobleBleClient) {
19
- super(nobleClient);
20
53
  this.#nobleClient = nobleClient;
54
+ this.#nobleClient.setDiscoveryCallback((address, manufacturerData) =>
55
+ this.#handleDiscoveredDevice(address, manufacturerData),
56
+ );
57
+ }
58
+
59
+ public getDiscoveredDevice(address: string): DiscoveredBleDevice {
60
+ const device = this.#discoveredMatterDevices.get(address);
61
+ if (device === undefined) {
62
+ throw new BleError(`No device found for address ${address}`);
63
+ }
64
+ return device;
65
+ }
66
+
67
+ /**
68
+ * Registers a deferred promise for a specific queryId together with a timeout and return the promise.
69
+ * The promise will be resolved when the timer runs out latest.
70
+ */
71
+ async #registerWaiterPromise(
72
+ queryId: string,
73
+ timeout?: Duration,
74
+ resolveOnUpdatedRecords = true,
75
+ cancelResolver?: (value: void) => void,
76
+ ) {
77
+ const { promise, resolver } = createPromise<void>();
78
+ let timer;
79
+ if (timeout) {
80
+ timer = Time.getTimer("BLE query timeout", timeout, () => {
81
+ cancelResolver?.();
82
+ this.#finishWaiter(queryId, true);
83
+ }).start();
84
+ }
85
+ this.#recordWaiters.set(queryId, { resolver, timer, resolveOnUpdatedRecords, cancelResolver });
86
+ logger.debug(
87
+ `Registered waiter for query ${queryId} with timeout ${timeout === undefined ? "(none)" : Duration.format(timeout)} ${
88
+ resolveOnUpdatedRecords ? "" : " (not resolving on updated records)"
89
+ }`,
90
+ );
91
+ await promise;
92
+ }
93
+
94
+ /**
95
+ * Remove a waiter promise for a specific queryId and stop the connected timer. If required also resolve the
96
+ * promise.
97
+ */
98
+ #finishWaiter(queryId: string, resolvePromise: boolean, isUpdatedRecord = false) {
99
+ const waiter = this.#recordWaiters.get(queryId);
100
+ if (waiter === undefined) return;
101
+ const { timer, resolver, resolveOnUpdatedRecords } = waiter;
102
+ if (isUpdatedRecord && !resolveOnUpdatedRecords) return;
103
+ logger.debug(`Finishing waiter for query ${queryId}, resolving: ${resolvePromise}`);
104
+ timer?.stop();
105
+ if (resolvePromise) {
106
+ resolver();
107
+ }
108
+ this.#recordWaiters.delete(queryId);
109
+ }
110
+
111
+ cancelCommissionableDeviceDiscovery(identifier: CommissionableDeviceIdentifiers, resolvePromise = true) {
112
+ const queryKey = this.#buildCommissionableQueryIdentifier(identifier);
113
+ const { cancelResolver } = this.#recordWaiters.get(queryKey) ?? {};
114
+ // Mark as canceled to not loop further in discovery, if cancel-resolver is used
115
+ cancelResolver?.();
116
+ this.#finishWaiter(queryKey, resolvePromise);
117
+ }
118
+
119
+ #handleDiscoveredDevice(peripheral: Peripheral, manufacturerServiceData: Bytes) {
120
+ const address = peripheral.address;
121
+
122
+ try {
123
+ const { discriminator, vendorId, productId, hasAdditionalAdvertisementData } =
124
+ BtpCodec.decodeBleAdvertisementServiceData(manufacturerServiceData);
125
+
126
+ const deviceData: CommissionableDeviceData = {
127
+ deviceIdentifier: address,
128
+ D: discriminator,
129
+ SD: (discriminator >> 8) & 0x0f,
130
+ VP: `${vendorId}+${productId}`,
131
+ CM: 1, // Can be no other mode,
132
+ addresses: [{ type: "ble", peripheralAddress: address }],
133
+ };
134
+ const deviceExisting = this.#discoveredMatterDevices.has(address);
135
+
136
+ logger.debug(
137
+ `${deviceExisting ? "Re-" : ""}Discovered device ${address} data: ${Diagnostic.json(deviceData)}`,
138
+ );
139
+
140
+ this.#discoveredMatterDevices.set(address, {
141
+ deviceData,
142
+ peripheral,
143
+ hasAdditionalAdvertisementData,
144
+ });
145
+
146
+ const queryKey = this.#findCommissionableQueryIdentifier(deviceData);
147
+ if (queryKey !== undefined) {
148
+ this.#finishWaiter(queryKey, true, deviceExisting);
149
+ }
150
+ } catch (error) {
151
+ logger.debug(
152
+ `Discovered device ${address} ${manufacturerServiceData === undefined ? undefined : Bytes.toHex(manufacturerServiceData)} does not seem to be a valid Matter device: ${error}`,
153
+ );
154
+ }
155
+ }
156
+
157
+ #findCommissionableQueryIdentifier(record: CommissionableDeviceData) {
158
+ const longDiscriminatorQueryId = this.#buildCommissionableQueryIdentifier({ longDiscriminator: record.D });
159
+ if (this.#recordWaiters.has(longDiscriminatorQueryId)) {
160
+ return longDiscriminatorQueryId;
161
+ }
162
+
163
+ const shortDiscriminatorQueryId = this.#buildCommissionableQueryIdentifier({ shortDiscriminator: record.SD });
164
+ if (this.#recordWaiters.has(shortDiscriminatorQueryId)) {
165
+ return shortDiscriminatorQueryId;
166
+ }
167
+
168
+ if (record.VP !== undefined) {
169
+ const vpParts = record.VP.split("+");
170
+ const vendorIdQueryId = this.#buildCommissionableQueryIdentifier({
171
+ vendorId: VendorId(parseInt(vpParts[0])),
172
+ });
173
+ if (this.#recordWaiters.has(vendorIdQueryId)) {
174
+ return vendorIdQueryId;
175
+ }
176
+ if (vpParts[1] !== undefined) {
177
+ const productIdQueryId = this.#buildCommissionableQueryIdentifier({
178
+ productId: parseInt(vpParts[1]),
179
+ });
180
+ if (this.#recordWaiters.has(productIdQueryId)) {
181
+ return productIdQueryId;
182
+ }
183
+ }
184
+ }
185
+
186
+ if (this.#recordWaiters.has("*")) {
187
+ return "*";
188
+ }
189
+
190
+ return undefined;
191
+ }
192
+
193
+ /**
194
+ * Builds an identifier string for commissionable queries based on the given identifier object.
195
+ * Some identifiers are identical to the official DNS-SD identifiers, others are custom.
196
+ */
197
+ #buildCommissionableQueryIdentifier(identifier: CommissionableDeviceIdentifiers) {
198
+ if ("longDiscriminator" in identifier) {
199
+ return `D:${identifier.longDiscriminator}`;
200
+ } else if ("shortDiscriminator" in identifier) {
201
+ return `SD:${identifier.shortDiscriminator}`;
202
+ } else if ("vendorId" in identifier) {
203
+ return `V:${identifier.vendorId}`;
204
+ } else if ("productId" in identifier) {
205
+ // Custom identifier because normally productId is only included in TXT record
206
+ return `P:${identifier.productId}`;
207
+ } else return "*";
208
+ }
209
+
210
+ #getCommissionableDevices(identifier: CommissionableDeviceIdentifiers) {
211
+ const storedRecords = Array.from(this.#discoveredMatterDevices.values());
212
+
213
+ const foundRecords = new Array<DiscoveredBleDevice>();
214
+ if ("longDiscriminator" in identifier) {
215
+ foundRecords.push(...storedRecords.filter(({ deviceData: { D } }) => D === identifier.longDiscriminator));
216
+ } else if ("shortDiscriminator" in identifier) {
217
+ foundRecords.push(
218
+ ...storedRecords.filter(({ deviceData: { SD } }) => SD === identifier.shortDiscriminator),
219
+ );
220
+ } else if ("vendorId" in identifier) {
221
+ foundRecords.push(
222
+ ...storedRecords.filter(
223
+ ({ deviceData: { VP } }) =>
224
+ VP === `${identifier.vendorId}` || VP?.startsWith(`${identifier.vendorId}+`),
225
+ ),
226
+ );
227
+ } else if ("productId" in identifier) {
228
+ foundRecords.push(
229
+ ...storedRecords.filter(({ deviceData: { VP } }) => VP?.endsWith(`+${identifier.productId}`)),
230
+ );
231
+ } else {
232
+ foundRecords.push(...storedRecords.filter(({ deviceData: { CM } }) => CM === 1 || CM === 2));
233
+ }
234
+
235
+ return foundRecords;
236
+ }
237
+
238
+ async findOperationalDevice(): Promise<undefined> {
239
+ logger.info(`skip BLE scan because scanning for operational devices is not supported`);
240
+ return undefined;
241
+ }
242
+
243
+ getDiscoveredOperationalDevice(): undefined {
244
+ logger.info(`skip BLE scan because scanning for operational devices is not supported`);
245
+ return undefined;
246
+ }
247
+
248
+ async findCommissionableDevices(
249
+ identifier: CommissionableDeviceIdentifiers,
250
+ timeout = Seconds(10),
251
+ ignoreExistingRecords = false,
252
+ ): Promise<CommissionableDevice[]> {
253
+ let storedRecords = this.#getCommissionableDevices(identifier);
254
+ if (ignoreExistingRecords) {
255
+ // We want to have a fresh discovery result, so clear out the stored records because they might be outdated
256
+ for (const record of storedRecords) {
257
+ this.#discoveredMatterDevices.delete(record.peripheral.address);
258
+ }
259
+ storedRecords = [];
260
+ }
261
+ if (storedRecords.length === 0) {
262
+ const queryKey = this.#buildCommissionableQueryIdentifier(identifier);
263
+
264
+ await this.#nobleClient.startScanning();
265
+ await this.#registerWaiterPromise(queryKey, timeout);
266
+
267
+ storedRecords = this.#getCommissionableDevices(identifier);
268
+ await this.#nobleClient.stopScanning();
269
+ }
270
+ return storedRecords.map(({ deviceData }) => deviceData);
271
+ }
272
+
273
+ async findCommissionableDevicesContinuously(
274
+ identifier: CommissionableDeviceIdentifiers,
275
+ callback: (device: CommissionableDevice) => void,
276
+ timeout?: Duration,
277
+ cancelSignal?: Promise<void>,
278
+ ): Promise<CommissionableDevice[]> {
279
+ const discoveredDevices = new Set<string>();
280
+
281
+ const discoveryEndTime = timeout ? Time.nowMs + timeout : undefined;
282
+ const queryKey = this.#buildCommissionableQueryIdentifier(identifier);
283
+ await this.#nobleClient.startScanning();
284
+
285
+ let queryResolver: ((value: void) => void) | undefined;
286
+ if (cancelSignal === undefined) {
287
+ const { promise, resolver } = createPromise<void>();
288
+ cancelSignal = promise;
289
+ queryResolver = resolver;
290
+ }
291
+
292
+ let canceled = false;
293
+ cancelSignal?.then(
294
+ () => {
295
+ canceled = true;
296
+ this.#finishWaiter(queryKey, true);
297
+ },
298
+ cause => {
299
+ logger.error("Unexpected error canceling commissioning", cause);
300
+ },
301
+ );
302
+
303
+ while (!canceled) {
304
+ this.#getCommissionableDevices(identifier).forEach(({ deviceData }) => {
305
+ const { deviceIdentifier } = deviceData;
306
+ if (!discoveredDevices.has(deviceIdentifier)) {
307
+ discoveredDevices.add(deviceIdentifier);
308
+ callback(deviceData);
309
+ }
310
+ });
311
+
312
+ let remainingTime;
313
+ if (discoveryEndTime !== undefined) {
314
+ remainingTime = Millis.ceil(Timespan(Time.nowMs, discoveryEndTime).duration);
315
+ if (remainingTime <= 0) {
316
+ break;
317
+ }
318
+ }
319
+
320
+ await this.#registerWaiterPromise(queryKey, remainingTime, false, queryResolver);
321
+ }
322
+ await this.#nobleClient.stopScanning();
323
+ return this.#getCommissionableDevices(identifier).map(({ deviceData }) => deviceData);
21
324
  }
22
325
 
23
- override getDiscoveredDevice(address: string): NobleDiscoveredBleDevice {
24
- return super.getDiscoveredDevice(address) as NobleDiscoveredBleDevice;
326
+ getDiscoveredCommissionableDevices(identifier: CommissionableDeviceIdentifiers): CommissionableDevice[] {
327
+ return this.#getCommissionableDevices(identifier).map(({ deviceData }) => deviceData);
25
328
  }
26
329
 
27
- protected override closeClient() {
330
+ async close() {
28
331
  this.#nobleClient.close();
332
+ [...this.#recordWaiters.keys()].forEach(queryId =>
333
+ this.#finishWaiter(queryId, !!this.#recordWaiters.get(queryId)?.timer),
334
+ );
29
335
  }
30
336
  }