@matter/nodejs-ble 0.11.0-alpha.0-20241005-e3e4e4a7a
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/LICENSE +201 -0
- package/README.md +55 -0
- package/dist/cjs/BleBroadcaster.d.ts +20 -0
- package/dist/cjs/BleBroadcaster.d.ts.map +1 -0
- package/dist/cjs/BleBroadcaster.js +121 -0
- package/dist/cjs/BleBroadcaster.js.map +6 -0
- package/dist/cjs/BlePeripheralInterface.d.ts +15 -0
- package/dist/cjs/BlePeripheralInterface.d.ts.map +1 -0
- package/dist/cjs/BlePeripheralInterface.js +54 -0
- package/dist/cjs/BlePeripheralInterface.js.map +6 -0
- package/dist/cjs/BleScanner.d.ts +52 -0
- package/dist/cjs/BleScanner.d.ts.map +1 -0
- package/dist/cjs/BleScanner.js +240 -0
- package/dist/cjs/BleScanner.js.map +6 -0
- package/dist/cjs/BlenoBleServer.d.ts +70 -0
- package/dist/cjs/BlenoBleServer.d.ts.map +1 -0
- package/dist/cjs/BlenoBleServer.js +337 -0
- package/dist/cjs/BlenoBleServer.js.map +6 -0
- package/dist/cjs/NobleBleChannel.d.ts +32 -0
- package/dist/cjs/NobleBleChannel.d.ts.map +1 -0
- package/dist/cjs/NobleBleChannel.js +266 -0
- package/dist/cjs/NobleBleChannel.js.map +6 -0
- package/dist/cjs/NobleBleClient.d.ts +20 -0
- package/dist/cjs/NobleBleClient.d.ts.map +1 -0
- package/dist/cjs/NobleBleClient.js +108 -0
- package/dist/cjs/NobleBleClient.js.map +6 -0
- package/dist/cjs/NodeJsBle.d.ts +22 -0
- package/dist/cjs/NodeJsBle.d.ts.map +1 -0
- package/dist/cjs/NodeJsBle.js +68 -0
- package/dist/cjs/NodeJsBle.js.map +6 -0
- package/dist/cjs/index.d.ts +9 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +26 -0
- package/dist/cjs/index.js.map +6 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/tsconfig.tsbuildinfo +1 -0
- package/dist/esm/BleBroadcaster.d.ts +20 -0
- package/dist/esm/BleBroadcaster.d.ts.map +1 -0
- package/dist/esm/BleBroadcaster.js +101 -0
- package/dist/esm/BleBroadcaster.js.map +6 -0
- package/dist/esm/BlePeripheralInterface.d.ts +15 -0
- package/dist/esm/BlePeripheralInterface.d.ts.map +1 -0
- package/dist/esm/BlePeripheralInterface.js +34 -0
- package/dist/esm/BlePeripheralInterface.js.map +6 -0
- package/dist/esm/BleScanner.d.ts +52 -0
- package/dist/esm/BleScanner.d.ts.map +1 -0
- package/dist/esm/BleScanner.js +220 -0
- package/dist/esm/BleScanner.js.map +6 -0
- package/dist/esm/BlenoBleServer.d.ts +70 -0
- package/dist/esm/BlenoBleServer.d.ts.map +1 -0
- package/dist/esm/BlenoBleServer.js +327 -0
- package/dist/esm/BlenoBleServer.js.map +6 -0
- package/dist/esm/NobleBleChannel.d.ts +32 -0
- package/dist/esm/NobleBleChannel.d.ts.map +1 -0
- package/dist/esm/NobleBleChannel.js +266 -0
- package/dist/esm/NobleBleChannel.js.map +6 -0
- package/dist/esm/NobleBleClient.d.ts +20 -0
- package/dist/esm/NobleBleClient.d.ts.map +1 -0
- package/dist/esm/NobleBleClient.js +88 -0
- package/dist/esm/NobleBleClient.js.map +6 -0
- package/dist/esm/NodeJsBle.d.ts +22 -0
- package/dist/esm/NodeJsBle.d.ts.map +1 -0
- package/dist/esm/NodeJsBle.js +48 -0
- package/dist/esm/NodeJsBle.js.map +6 -0
- package/dist/esm/index.d.ts +9 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/index.js.map +6 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/tsconfig.tsbuildinfo +1 -0
- package/package.json +83 -0
- package/require/package.json +4 -0
- package/require/require.cjs +1 -0
- package/require/require.d.ts +1 -0
- package/require/require.mjs +3 -0
- package/src/BleBroadcaster.ts +126 -0
- package/src/BlePeripheralInterface.ts +36 -0
- package/src/BleScanner.ts +279 -0
- package/src/BlenoBleServer.ts +403 -0
- package/src/NobleBleChannel.ts +337 -0
- package/src/NobleBleClient.ts +117 -0
- package/src/NodeJsBle.ts +56 -0
- package/src/index.ts +9 -0
- package/src/tsconfig.json +16 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2022-2024 Matter.js Authors
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
Channel,
|
|
9
|
+
ChannelType,
|
|
10
|
+
InternalError,
|
|
11
|
+
Logger,
|
|
12
|
+
NetInterface,
|
|
13
|
+
ServerAddress,
|
|
14
|
+
Time,
|
|
15
|
+
TransportInterface,
|
|
16
|
+
createPromise,
|
|
17
|
+
} from "@matter/general";
|
|
18
|
+
import {
|
|
19
|
+
BLE_MATTER_C1_CHARACTERISTIC_UUID,
|
|
20
|
+
BLE_MATTER_C2_CHARACTERISTIC_UUID,
|
|
21
|
+
BLE_MATTER_C3_CHARACTERISTIC_UUID,
|
|
22
|
+
BLE_MATTER_SERVICE_UUID,
|
|
23
|
+
BLE_MAXIMUM_BTP_MTU,
|
|
24
|
+
BTP_CONN_RSP_TIMEOUT_MS,
|
|
25
|
+
BTP_MAXIMUM_WINDOW_SIZE,
|
|
26
|
+
BTP_SUPPORTED_VERSIONS,
|
|
27
|
+
Ble,
|
|
28
|
+
BleChannel,
|
|
29
|
+
BleError,
|
|
30
|
+
BtpFlowError,
|
|
31
|
+
BtpSessionHandler,
|
|
32
|
+
} from "@project-chip/matter.js/ble";
|
|
33
|
+
import { BtpCodec } from "@project-chip/matter.js/codec";
|
|
34
|
+
import type { Characteristic, Peripheral } from "@stoprocent/noble";
|
|
35
|
+
import { BleScanner } from "./BleScanner.js";
|
|
36
|
+
|
|
37
|
+
const logger = Logger.get("BleChannel");
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Convert a UUID in noble's format to a proper UUID.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} uuid - UUID to convert
|
|
43
|
+
* @returns {string} UUID
|
|
44
|
+
*/
|
|
45
|
+
function nobleUuidToUuid(uuid: string): string {
|
|
46
|
+
uuid = uuid.toUpperCase();
|
|
47
|
+
|
|
48
|
+
if (uuid.length !== 32) {
|
|
49
|
+
return uuid;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const parts = [
|
|
53
|
+
uuid.substring(0, 8),
|
|
54
|
+
uuid.substring(8, 12),
|
|
55
|
+
uuid.substring(12, 16),
|
|
56
|
+
uuid.substring(16, 20),
|
|
57
|
+
uuid.substring(20, 32),
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
return parts.join("-");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class NobleBleCentralInterface implements NetInterface {
|
|
64
|
+
private openChannels: Map<ServerAddress, Peripheral> = new Map();
|
|
65
|
+
private onMatterMessageListener: ((socket: Channel<Uint8Array>, data: Uint8Array) => void) | undefined;
|
|
66
|
+
|
|
67
|
+
openChannel(address: ServerAddress, tryCount = 1): Promise<Channel<Uint8Array>> {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
if (this.onMatterMessageListener === undefined) {
|
|
70
|
+
reject(new InternalError(`Network Interface was not added to the system yet.`));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (address.type !== "ble") {
|
|
74
|
+
reject(new InternalError(`Unsupported address type ${address.type}.`));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
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
|
+
|
|
83
|
+
if (tryCount > 3) {
|
|
84
|
+
reject(new BleError(`Failed to connect to peripheral ${peripheral.address}`));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
logger.debug("BLE peripheral state", peripheral.state);
|
|
89
|
+
if (peripheral.state === "connected" || peripheral.state === "connecting") {
|
|
90
|
+
reject(
|
|
91
|
+
new BleError(
|
|
92
|
+
`Peripheral ${address.peripheralAddress} is already connected or connecting. Only one connection supported right now.`,
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (this.openChannels.has(address)) {
|
|
98
|
+
reject(
|
|
99
|
+
new BleError(
|
|
100
|
+
`Peripheral ${address.peripheralAddress} is already connected. Only one connection supported right now.`,
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
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
|
+
|
|
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;
|
|
161
|
+
}
|
|
162
|
+
|
|
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
|
+
}
|
|
176
|
+
|
|
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
|
+
if (error) {
|
|
182
|
+
logger.error(
|
|
183
|
+
`Peripheral ${peripheral.address} disconnected while trying to connect, try again`,
|
|
184
|
+
error,
|
|
185
|
+
);
|
|
186
|
+
} else {
|
|
187
|
+
logger.info(`Peripheral ${peripheral.address} disconnected while trying to connect, try gain`);
|
|
188
|
+
}
|
|
189
|
+
// Cleanup listeners and try again
|
|
190
|
+
peripheral.removeAllListeners("disconnect");
|
|
191
|
+
peripheral.removeAllListeners("connect");
|
|
192
|
+
this.openChannel(address, tryCount + 1)
|
|
193
|
+
.then(resolve)
|
|
194
|
+
.catch(reject);
|
|
195
|
+
};
|
|
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);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
onData(listener: (socket: Channel<Uint8Array>, data: Uint8Array) => void): TransportInterface.Listener {
|
|
204
|
+
this.onMatterMessageListener = listener;
|
|
205
|
+
return {
|
|
206
|
+
close: async () => await this.close(),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async close() {
|
|
211
|
+
for (const peripheral of this.openChannels.values()) {
|
|
212
|
+
await peripheral.disconnectAsync();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
supports(type: ChannelType, _address?: string) {
|
|
217
|
+
if (type !== ChannelType.BLE) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export class NobleBleChannel extends BleChannel<Uint8Array> {
|
|
225
|
+
static async create(
|
|
226
|
+
peripheral: Peripheral,
|
|
227
|
+
characteristicC1ForWrite: Characteristic,
|
|
228
|
+
characteristicC2ForSubscribe: Characteristic,
|
|
229
|
+
onMatterMessageListener: (socket: Channel<Uint8Array>, data: Uint8Array) => void,
|
|
230
|
+
_additionalCommissioningRelatedData?: Uint8Array,
|
|
231
|
+
): Promise<NobleBleChannel> {
|
|
232
|
+
let mtu = peripheral.mtu ?? 0;
|
|
233
|
+
if (mtu > BLE_MAXIMUM_BTP_MTU) {
|
|
234
|
+
mtu = BLE_MAXIMUM_BTP_MTU;
|
|
235
|
+
}
|
|
236
|
+
logger.debug(`Using MTU=${mtu} (Peripheral MTU=${peripheral.mtu})`);
|
|
237
|
+
const btpHandshakeRequest = BtpCodec.encodeBtpHandshakeRequest({
|
|
238
|
+
versions: BTP_SUPPORTED_VERSIONS,
|
|
239
|
+
attMtu: mtu,
|
|
240
|
+
clientWindowSize: BTP_MAXIMUM_WINDOW_SIZE,
|
|
241
|
+
});
|
|
242
|
+
logger.debug(`sending BTP handshake request: ${Logger.toJSON(btpHandshakeRequest)}`);
|
|
243
|
+
await characteristicC1ForWrite.writeAsync(Buffer.from(btpHandshakeRequest.buffer), false);
|
|
244
|
+
|
|
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");
|
|
251
|
+
await characteristicC2ForSubscribe.subscribeAsync();
|
|
252
|
+
|
|
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
|
+
});
|
|
265
|
+
|
|
266
|
+
const handshakeResponse = await handshakeResponseReceivedPromise;
|
|
267
|
+
|
|
268
|
+
const btpSession = await BtpSessionHandler.createAsCentral(
|
|
269
|
+
new Uint8Array(handshakeResponse),
|
|
270
|
+
// callback to write data to characteristic C1
|
|
271
|
+
async (data: Uint8Array) => {
|
|
272
|
+
return await characteristicC1ForWrite.writeAsync(Buffer.from(data.buffer), false);
|
|
273
|
+
},
|
|
274
|
+
// callback to disconnect the BLE connection
|
|
275
|
+
async () =>
|
|
276
|
+
void characteristicC2ForSubscribe
|
|
277
|
+
.unsubscribeAsync()
|
|
278
|
+
.then(() => peripheral.disconnectAsync().then(() => logger.debug("disconnected from peripheral"))),
|
|
279
|
+
// callback to forward decoded and de-assembled Matter messages to ExchangeManager
|
|
280
|
+
async (data: Uint8Array) => {
|
|
281
|
+
if (onMatterMessageListener === undefined) {
|
|
282
|
+
throw new InternalError(`No listener registered for Matter messages`);
|
|
283
|
+
}
|
|
284
|
+
onMatterMessageListener(nobleChannel, data);
|
|
285
|
+
},
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
characteristicC2ForSubscribe.on("data", (data, isNotification) => {
|
|
289
|
+
logger.debug(`received data on C2: ${data.toString("hex")} (isNotification: ${isNotification})`);
|
|
290
|
+
|
|
291
|
+
void btpSession.handleIncomingBleData(new Uint8Array(data));
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const nobleChannel = new NobleBleChannel(peripheral, btpSession);
|
|
295
|
+
return nobleChannel;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private connected = true;
|
|
299
|
+
|
|
300
|
+
constructor(
|
|
301
|
+
private readonly peripheral: Peripheral,
|
|
302
|
+
private readonly btpSession: BtpSessionHandler,
|
|
303
|
+
) {
|
|
304
|
+
super();
|
|
305
|
+
peripheral.once("disconnect", () => {
|
|
306
|
+
logger.debug(`Disconnected from peripheral ${peripheral.address}`);
|
|
307
|
+
this.connected = false;
|
|
308
|
+
void this.btpSession.close();
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Send a Matter message to the connected device - need to do BTP assembly first.
|
|
314
|
+
*
|
|
315
|
+
* @param data
|
|
316
|
+
*/
|
|
317
|
+
async send(data: Uint8Array) {
|
|
318
|
+
if (!this.connected) {
|
|
319
|
+
logger.debug("Cannot send data because not connected to peripheral.");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (this.btpSession === undefined) {
|
|
323
|
+
throw new BtpFlowError(`Cannot send data, no BTP session initialized`);
|
|
324
|
+
}
|
|
325
|
+
await this.btpSession.sendMatterMessage(data);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Channel<Uint8Array>
|
|
329
|
+
get name() {
|
|
330
|
+
return `${this.type}://${this.peripheral.address}`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async close() {
|
|
334
|
+
await this.btpSession.close();
|
|
335
|
+
await this.peripheral.disconnectAsync();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2022-2024 Matter.js Authors
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Logger } from "@matter/general";
|
|
8
|
+
import { require } from "@matter/nodejs-ble/require";
|
|
9
|
+
import { BLE_MATTER_SERVICE_UUID } from "@project-chip/matter.js/ble";
|
|
10
|
+
import type { Peripheral } from "@stoprocent/noble";
|
|
11
|
+
import { BleOptions } from "./NodeJsBle.js";
|
|
12
|
+
|
|
13
|
+
const logger = Logger.get("NobleBleClient");
|
|
14
|
+
let noble: typeof import("@stoprocent/noble");
|
|
15
|
+
|
|
16
|
+
function loadNoble(hciId?: number) {
|
|
17
|
+
// load noble driver with the correct device selected
|
|
18
|
+
if (hciId !== undefined) {
|
|
19
|
+
process.env.NOBLE_HCI_DEVICE_ID = hciId.toString();
|
|
20
|
+
}
|
|
21
|
+
noble = require("@stoprocent/noble");
|
|
22
|
+
if (typeof noble.on !== "function") {
|
|
23
|
+
// The following commit broke the default exported instance of noble:
|
|
24
|
+
// https://github.com/abandonware/noble/commit/b67eea246f719947fc45b1b52b856e61637a8a8e
|
|
25
|
+
noble = (noble as any)({ extended: false });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class NobleBleClient {
|
|
30
|
+
private readonly discoveredPeripherals = new Map<
|
|
31
|
+
string,
|
|
32
|
+
{ peripheral: Peripheral; matterServiceData: Uint8Array }
|
|
33
|
+
>();
|
|
34
|
+
private shouldScan = false;
|
|
35
|
+
private isScanning = false;
|
|
36
|
+
private nobleState = "unknown";
|
|
37
|
+
private deviceDiscoveredCallback: ((peripheral: Peripheral, manufacturerData: Uint8Array) => void) | undefined;
|
|
38
|
+
|
|
39
|
+
constructor(options?: BleOptions) {
|
|
40
|
+
loadNoble(options?.hciId);
|
|
41
|
+
/*try {
|
|
42
|
+
noble.reset();
|
|
43
|
+
} catch (error: any) {
|
|
44
|
+
logger.debug(
|
|
45
|
+
`Error resetting BLE device via noble (can be ignored, we just tried): ${
|
|
46
|
+
(error as unknown as Error).message
|
|
47
|
+
}`,
|
|
48
|
+
);
|
|
49
|
+
}*/
|
|
50
|
+
noble.on("stateChange", state => {
|
|
51
|
+
this.nobleState = state;
|
|
52
|
+
logger.debug(`Noble state changed to ${state}`);
|
|
53
|
+
if (state === "poweredOn") {
|
|
54
|
+
if (this.shouldScan) {
|
|
55
|
+
void this.startScanning();
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
void this.stopScanning();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
noble.on("discover", peripheral => this.handleDiscoveredDevice(peripheral));
|
|
62
|
+
noble.on("scanStart", () => (this.isScanning = true));
|
|
63
|
+
noble.on("scanStop", () => (this.isScanning = false));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public setDiscoveryCallback(callback: (peripheral: Peripheral, manufacturerData: Uint8Array) => void) {
|
|
67
|
+
this.deviceDiscoveredCallback = callback;
|
|
68
|
+
for (const { peripheral, matterServiceData } of this.discoveredPeripherals.values()) {
|
|
69
|
+
this.deviceDiscoveredCallback(peripheral, matterServiceData);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public async startScanning() {
|
|
74
|
+
if (this.isScanning) return;
|
|
75
|
+
|
|
76
|
+
this.shouldScan = true;
|
|
77
|
+
if (this.nobleState === "poweredOn") {
|
|
78
|
+
logger.debug("Start BLE scanning for Matter Services ...");
|
|
79
|
+
await noble.startScanningAsync([BLE_MATTER_SERVICE_UUID], false);
|
|
80
|
+
} else {
|
|
81
|
+
logger.debug("noble state is not poweredOn ... delay scanning till poweredOn");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public async stopScanning() {
|
|
86
|
+
this.shouldScan = false;
|
|
87
|
+
logger.debug("Stop BLE scanning for Matter Services ...");
|
|
88
|
+
await noble.stopScanningAsync();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private handleDiscoveredDevice(peripheral: Peripheral) {
|
|
92
|
+
// The advertisement data contains a name, power level (if available), certain advertised service uuids,
|
|
93
|
+
// as well as manufacturer data.
|
|
94
|
+
// {"localName":"MATTER-3840","serviceData":[{"uuid":"fff6","data":{"type":"Buffer","data":[0,0,15,241,255,1,128,0]}}],"serviceUuids":["fff6"],"solicitationServiceUuids":[],"serviceSolicitationUuids":[]}
|
|
95
|
+
logger.debug(
|
|
96
|
+
`Found peripheral ${peripheral.address} (${peripheral.advertisement.localName}): ${Logger.toJSON(
|
|
97
|
+
peripheral.advertisement,
|
|
98
|
+
)}`,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (!peripheral.connectable) {
|
|
102
|
+
logger.info(`Peripheral ${peripheral.address} is not connectable ... ignoring`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const matterServiceData = peripheral.advertisement.serviceData.find(
|
|
106
|
+
serviceData => serviceData.uuid === BLE_MATTER_SERVICE_UUID,
|
|
107
|
+
);
|
|
108
|
+
if (matterServiceData === undefined || matterServiceData.data.length !== 8) {
|
|
109
|
+
logger.info(`Peripheral ${peripheral.address} does not advertise Matter Service ... ignoring`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.discoveredPeripherals.set(peripheral.address, { peripheral, matterServiceData: matterServiceData.data });
|
|
114
|
+
|
|
115
|
+
this.deviceDiscoveredCallback?.(peripheral, matterServiceData.data);
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/NodeJsBle.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2022-2024 Matter.js Authors
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { NetInterface, TransportInterface } from "@matter/general";
|
|
8
|
+
import { Ble } from "@project-chip/matter.js/ble";
|
|
9
|
+
import { InstanceBroadcaster, Scanner } from "@project-chip/matter.js/common";
|
|
10
|
+
import { BleBroadcaster } from "./BleBroadcaster.js";
|
|
11
|
+
import { BlePeripheralInterface } from "./BlePeripheralInterface.js";
|
|
12
|
+
import { BleScanner } from "./BleScanner.js";
|
|
13
|
+
import { BlenoBleServer } from "./BlenoBleServer.js";
|
|
14
|
+
import { NobleBleCentralInterface } from "./NobleBleChannel.js";
|
|
15
|
+
import { NobleBleClient } from "./NobleBleClient.js";
|
|
16
|
+
|
|
17
|
+
export type BleOptions = {
|
|
18
|
+
hciId?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class NodeJsBle extends Ble {
|
|
22
|
+
private blePeripheral: BlenoBleServer | undefined;
|
|
23
|
+
private bleCentral: NobleBleClient | undefined;
|
|
24
|
+
|
|
25
|
+
constructor(private readonly options?: BleOptions) {
|
|
26
|
+
super();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getBlePeripheralInterface(): TransportInterface {
|
|
30
|
+
if (this.blePeripheral === undefined) {
|
|
31
|
+
this.blePeripheral = new BlenoBleServer(this.options);
|
|
32
|
+
}
|
|
33
|
+
return new BlePeripheralInterface(this.blePeripheral);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getBleCentralInterface(): NetInterface {
|
|
37
|
+
if (this.bleCentral === undefined) {
|
|
38
|
+
this.bleCentral = new NobleBleClient(this.options);
|
|
39
|
+
}
|
|
40
|
+
return new NobleBleCentralInterface();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getBleBroadcaster(additionalAdvertisementData?: Uint8Array): InstanceBroadcaster {
|
|
44
|
+
if (this.blePeripheral === undefined) {
|
|
45
|
+
this.blePeripheral = new BlenoBleServer(this.options);
|
|
46
|
+
}
|
|
47
|
+
return new BleBroadcaster(this.blePeripheral, additionalAdvertisementData);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getBleScanner(): Scanner {
|
|
51
|
+
if (this.bleCentral === undefined) {
|
|
52
|
+
this.bleCentral = new NobleBleClient(this.options);
|
|
53
|
+
}
|
|
54
|
+
return new BleScanner(this.bleCentral);
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/index.ts
ADDED