@matter/nodejs-ble 0.12.0-alpha.0-20250103-ba0c01a01 → 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.
- package/dist/cjs/BleScanner.d.ts +2 -2
- package/dist/cjs/BleScanner.d.ts.map +1 -1
- package/dist/cjs/BleScanner.js +19 -13
- package/dist/cjs/BleScanner.js.map +1 -1
- package/dist/cjs/BlenoBleServer.d.ts.map +1 -1
- package/dist/cjs/BlenoBleServer.js +4 -4
- package/dist/cjs/BlenoBleServer.js.map +1 -1
- package/dist/cjs/NobleBleChannel.d.ts +3 -2
- package/dist/cjs/NobleBleChannel.d.ts.map +1 -1
- package/dist/cjs/NobleBleChannel.js +252 -101
- package/dist/cjs/NobleBleChannel.js.map +2 -2
- package/dist/cjs/NobleBleClient.d.ts.map +1 -1
- package/dist/cjs/NobleBleClient.js +15 -7
- package/dist/cjs/NobleBleClient.js.map +1 -1
- package/dist/cjs/NodeJsBle.d.ts +1 -2
- package/dist/cjs/NodeJsBle.d.ts.map +1 -1
- package/dist/cjs/NodeJsBle.js +30 -14
- package/dist/cjs/NodeJsBle.js.map +1 -1
- package/dist/esm/BleScanner.d.ts +2 -2
- package/dist/esm/BleScanner.d.ts.map +1 -1
- package/dist/esm/BleScanner.js +19 -13
- package/dist/esm/BleScanner.js.map +1 -1
- package/dist/esm/BlenoBleServer.d.ts.map +1 -1
- package/dist/esm/BlenoBleServer.js +4 -4
- package/dist/esm/BlenoBleServer.js.map +1 -1
- package/dist/esm/NobleBleChannel.d.ts +3 -2
- package/dist/esm/NobleBleChannel.d.ts.map +1 -1
- package/dist/esm/NobleBleChannel.js +253 -102
- package/dist/esm/NobleBleChannel.js.map +2 -2
- package/dist/esm/NobleBleClient.d.ts.map +1 -1
- package/dist/esm/NobleBleClient.js +15 -7
- package/dist/esm/NobleBleClient.js.map +1 -1
- package/dist/esm/NodeJsBle.d.ts +1 -2
- package/dist/esm/NodeJsBle.d.ts.map +1 -1
- package/dist/esm/NodeJsBle.js +30 -14
- package/dist/esm/NodeJsBle.js.map +1 -1
- package/package.json +5 -5
- package/src/BleScanner.ts +23 -13
- package/src/BlenoBleServer.ts +6 -4
- package/src/NobleBleChannel.ts +316 -126
- package/src/NobleBleClient.ts +17 -7
- package/src/NodeJsBle.ts +32 -14
package/src/NobleBleChannel.ts
CHANGED
|
@@ -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
|
-
|
|
65
|
-
|
|
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
|
|
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 ${
|
|
97
|
+
reject(new BleError(`Failed to connect to peripheral ${peripheralAddress}`));
|
|
85
98
|
return;
|
|
86
99
|
}
|
|
87
100
|
|
|
88
|
-
|
|
89
|
-
|
|
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 ${
|
|
108
|
+
`Peripheral ${peripheralAddress} is already connected. Only one connection supported right now.`,
|
|
93
109
|
),
|
|
94
110
|
);
|
|
95
111
|
return;
|
|
96
112
|
}
|
|
97
|
-
if (this.
|
|
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
|
-
`
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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 ${
|
|
185
|
+
`Peripheral ${peripheralAddress} disconnected while trying to connect, try again`,
|
|
184
186
|
error,
|
|
185
187
|
);
|
|
186
188
|
} else {
|
|
187
|
-
logger.info(`Peripheral ${
|
|
189
|
+
logger.info(`Peripheral ${peripheralAddress} disconnected while trying to connect, try again`);
|
|
188
190
|
}
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
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
|
-
|
|
212
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(() =>
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
523
|
+
this.peripheral
|
|
524
|
+
.disconnectAsync()
|
|
525
|
+
.catch(error => logger.error(`Peripheral ${this.peripheral.address}: Error while disconnecting`, error));
|
|
336
526
|
}
|
|
337
527
|
}
|
package/src/NobleBleClient.ts
CHANGED
|
@@ -59,7 +59,14 @@ export class NobleBleClient {
|
|
|
59
59
|
}
|
|
60
60
|
});
|
|
61
61
|
noble.on("discover", peripheral => this.handleDiscoveredDevice(peripheral));
|
|
62
|
-
noble.on("scanStart", () =>
|
|
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)
|
|
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],
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
121
|
+
logger.info(`Peripheral ${address} does not advertise Matter Service ... ignoring`);
|
|
112
122
|
return;
|
|
113
123
|
}
|
|
114
124
|
|
|
115
|
-
this.discoveredPeripherals.set(
|
|
125
|
+
this.discoveredPeripherals.set(address, { peripheral, matterServiceData: matterServiceData.data });
|
|
116
126
|
|
|
117
127
|
this.deviceDiscoveredCallback?.(peripheral, matterServiceData.data);
|
|
118
128
|
}
|