@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.
Files changed (84) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +55 -0
  3. package/dist/cjs/BleBroadcaster.d.ts +20 -0
  4. package/dist/cjs/BleBroadcaster.d.ts.map +1 -0
  5. package/dist/cjs/BleBroadcaster.js +121 -0
  6. package/dist/cjs/BleBroadcaster.js.map +6 -0
  7. package/dist/cjs/BlePeripheralInterface.d.ts +15 -0
  8. package/dist/cjs/BlePeripheralInterface.d.ts.map +1 -0
  9. package/dist/cjs/BlePeripheralInterface.js +54 -0
  10. package/dist/cjs/BlePeripheralInterface.js.map +6 -0
  11. package/dist/cjs/BleScanner.d.ts +52 -0
  12. package/dist/cjs/BleScanner.d.ts.map +1 -0
  13. package/dist/cjs/BleScanner.js +240 -0
  14. package/dist/cjs/BleScanner.js.map +6 -0
  15. package/dist/cjs/BlenoBleServer.d.ts +70 -0
  16. package/dist/cjs/BlenoBleServer.d.ts.map +1 -0
  17. package/dist/cjs/BlenoBleServer.js +337 -0
  18. package/dist/cjs/BlenoBleServer.js.map +6 -0
  19. package/dist/cjs/NobleBleChannel.d.ts +32 -0
  20. package/dist/cjs/NobleBleChannel.d.ts.map +1 -0
  21. package/dist/cjs/NobleBleChannel.js +266 -0
  22. package/dist/cjs/NobleBleChannel.js.map +6 -0
  23. package/dist/cjs/NobleBleClient.d.ts +20 -0
  24. package/dist/cjs/NobleBleClient.d.ts.map +1 -0
  25. package/dist/cjs/NobleBleClient.js +108 -0
  26. package/dist/cjs/NobleBleClient.js.map +6 -0
  27. package/dist/cjs/NodeJsBle.d.ts +22 -0
  28. package/dist/cjs/NodeJsBle.d.ts.map +1 -0
  29. package/dist/cjs/NodeJsBle.js +68 -0
  30. package/dist/cjs/NodeJsBle.js.map +6 -0
  31. package/dist/cjs/index.d.ts +9 -0
  32. package/dist/cjs/index.d.ts.map +1 -0
  33. package/dist/cjs/index.js +26 -0
  34. package/dist/cjs/index.js.map +6 -0
  35. package/dist/cjs/package.json +3 -0
  36. package/dist/cjs/tsconfig.tsbuildinfo +1 -0
  37. package/dist/esm/BleBroadcaster.d.ts +20 -0
  38. package/dist/esm/BleBroadcaster.d.ts.map +1 -0
  39. package/dist/esm/BleBroadcaster.js +101 -0
  40. package/dist/esm/BleBroadcaster.js.map +6 -0
  41. package/dist/esm/BlePeripheralInterface.d.ts +15 -0
  42. package/dist/esm/BlePeripheralInterface.d.ts.map +1 -0
  43. package/dist/esm/BlePeripheralInterface.js +34 -0
  44. package/dist/esm/BlePeripheralInterface.js.map +6 -0
  45. package/dist/esm/BleScanner.d.ts +52 -0
  46. package/dist/esm/BleScanner.d.ts.map +1 -0
  47. package/dist/esm/BleScanner.js +220 -0
  48. package/dist/esm/BleScanner.js.map +6 -0
  49. package/dist/esm/BlenoBleServer.d.ts +70 -0
  50. package/dist/esm/BlenoBleServer.d.ts.map +1 -0
  51. package/dist/esm/BlenoBleServer.js +327 -0
  52. package/dist/esm/BlenoBleServer.js.map +6 -0
  53. package/dist/esm/NobleBleChannel.d.ts +32 -0
  54. package/dist/esm/NobleBleChannel.d.ts.map +1 -0
  55. package/dist/esm/NobleBleChannel.js +266 -0
  56. package/dist/esm/NobleBleChannel.js.map +6 -0
  57. package/dist/esm/NobleBleClient.d.ts +20 -0
  58. package/dist/esm/NobleBleClient.d.ts.map +1 -0
  59. package/dist/esm/NobleBleClient.js +88 -0
  60. package/dist/esm/NobleBleClient.js.map +6 -0
  61. package/dist/esm/NodeJsBle.d.ts +22 -0
  62. package/dist/esm/NodeJsBle.d.ts.map +1 -0
  63. package/dist/esm/NodeJsBle.js +48 -0
  64. package/dist/esm/NodeJsBle.js.map +6 -0
  65. package/dist/esm/index.d.ts +9 -0
  66. package/dist/esm/index.d.ts.map +1 -0
  67. package/dist/esm/index.js +9 -0
  68. package/dist/esm/index.js.map +6 -0
  69. package/dist/esm/package.json +3 -0
  70. package/dist/esm/tsconfig.tsbuildinfo +1 -0
  71. package/package.json +83 -0
  72. package/require/package.json +4 -0
  73. package/require/require.cjs +1 -0
  74. package/require/require.d.ts +1 -0
  75. package/require/require.mjs +3 -0
  76. package/src/BleBroadcaster.ts +126 -0
  77. package/src/BlePeripheralInterface.ts +36 -0
  78. package/src/BleScanner.ts +279 -0
  79. package/src/BlenoBleServer.ts +403 -0
  80. package/src/NobleBleChannel.ts +337 -0
  81. package/src/NobleBleClient.ts +117 -0
  82. package/src/NodeJsBle.ts +56 -0
  83. package/src/index.ts +9 -0
  84. package/src/tsconfig.json +16 -0
@@ -0,0 +1,403 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2024 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Channel, InternalError, Logger, Time, createPromise } from "@matter/general";
8
+ import { require } from "@matter/nodejs-ble/require";
9
+ import {
10
+ BLE_MATTER_C1_CHARACTERISTIC_UUID,
11
+ BLE_MATTER_C2_CHARACTERISTIC_UUID,
12
+ BLE_MATTER_C3_CHARACTERISTIC_UUID,
13
+ BLE_MATTER_SERVICE_UUID,
14
+ BTP_CONN_RSP_TIMEOUT_MS,
15
+ BleChannel,
16
+ BleError,
17
+ BtpFlowError,
18
+ BtpSessionHandler,
19
+ } from "@project-chip/matter.js/ble";
20
+ import { ChannelNotConnectedError } from "@project-chip/matter.js/protocol";
21
+ import { BleOptions } from "./NodeJsBle.js";
22
+
23
+ const logger = Logger.get("BlenoBleServer");
24
+ let Bleno: typeof import("@stoprocent/bleno");
25
+
26
+ function initializeBleno(server: BlenoBleServer, hciId?: number) {
27
+ // load Bleno driver with the correct device selected
28
+ if (hciId !== undefined) {
29
+ process.env.BLENO_HCI_DEVICE_ID = hciId.toString();
30
+ }
31
+ Bleno = require("@stoprocent/bleno");
32
+
33
+ class BtpWriteCharacteristicC1 extends Bleno.Characteristic {
34
+ constructor() {
35
+ super({
36
+ uuid: BLE_MATTER_C1_CHARACTERISTIC_UUID,
37
+ properties: ["write"],
38
+ });
39
+ }
40
+
41
+ override onWriteRequest(
42
+ data: Buffer,
43
+ offset: number,
44
+ withoutResponse: boolean,
45
+ callback: (result: number) => void,
46
+ ) {
47
+ logger.debug(`C1 write request: ${data.toString("hex")} ${offset} ${withoutResponse}`);
48
+
49
+ try {
50
+ server.handleC1WriteRequest(data, offset, withoutResponse);
51
+ callback(this.RESULT_SUCCESS);
52
+ } catch (e) {
53
+ logger.error(`C1 write request failed: ${e}`);
54
+ callback(this.RESULT_UNLIKELY_ERROR);
55
+ }
56
+ }
57
+ }
58
+
59
+ class BtpIndicateCharacteristicC2 extends Bleno.Characteristic {
60
+ constructor() {
61
+ super({
62
+ uuid: BLE_MATTER_C2_CHARACTERISTIC_UUID,
63
+ properties: ["indicate"],
64
+ });
65
+ }
66
+
67
+ override async onSubscribe(maxValueSize: number, updateValueCallback: (data: Buffer) => void) {
68
+ logger.debug(`C2 subscribe ${maxValueSize}`);
69
+
70
+ await server.handleC2SubscribeRequest(maxValueSize, updateValueCallback);
71
+ }
72
+
73
+ override async onUnsubscribe() {
74
+ logger.debug("C2 unsubscribe");
75
+ await server.close();
76
+ }
77
+
78
+ override onIndicate() {
79
+ logger.debug("C2 indicate");
80
+ server.handleC2Indicate();
81
+ }
82
+ }
83
+
84
+ class BtpReadCharacteristicC3 extends Bleno.Characteristic {
85
+ constructor() {
86
+ super({
87
+ uuid: BLE_MATTER_C3_CHARACTERISTIC_UUID,
88
+ properties: ["read"],
89
+ });
90
+ }
91
+
92
+ override onReadRequest(offset: number, callback: (result: number, data?: Buffer) => void) {
93
+ try {
94
+ const data = server.handleC3ReadRequest(offset);
95
+ logger.debug(`C3 read request: ${data.toString("hex")} ${offset}`);
96
+ callback(this.RESULT_SUCCESS, data);
97
+ } catch (e) {
98
+ logger.debug(`C3 read request failed : ${e} ${offset}`);
99
+ callback(this.RESULT_INVALID_OFFSET);
100
+ }
101
+ }
102
+ }
103
+
104
+ class BtpService extends Bleno.PrimaryService {
105
+ constructor() {
106
+ super({
107
+ uuid: BLE_MATTER_SERVICE_UUID,
108
+ characteristics: [
109
+ new BtpWriteCharacteristicC1(),
110
+ new BtpIndicateCharacteristicC2(),
111
+ new BtpReadCharacteristicC3(),
112
+ ],
113
+ });
114
+ }
115
+ }
116
+
117
+ return new BtpService();
118
+ }
119
+
120
+ /**
121
+ * Implements the Matter over BLE server using Bleno as Peripheral.
122
+ *
123
+ * Note: Bleno is only supporting a single connection at a time right now - mainly because it also only can announce
124
+ * one BLE device at a time!
125
+ */
126
+ export class BlenoBleServer extends BleChannel<Uint8Array> {
127
+ private state = "unknown";
128
+ isAdvertising = false;
129
+ private additionalAdvertisingData: Buffer = Buffer.alloc(0);
130
+ private advertisingData: Buffer | undefined;
131
+
132
+ private latestHandshakePayload: Buffer | undefined;
133
+ private btpSession: BtpSessionHandler | undefined;
134
+
135
+ private onMatterMessageListener: ((socket: Channel<Uint8Array>, data: Uint8Array) => void) | undefined;
136
+ private writeConformationResolver: ((value: void) => void) | undefined;
137
+
138
+ public clientAddress: string | undefined;
139
+ private btpHandshakeTimeout = Time.getTimer("BTP handshake timeout", BTP_CONN_RSP_TIMEOUT_MS, () =>
140
+ this.btpHandshakeTimeoutTriggered(),
141
+ );
142
+
143
+ private readonly matterBleService;
144
+
145
+ constructor(options?: BleOptions) {
146
+ super();
147
+ this.matterBleService = initializeBleno(this, options?.hciId);
148
+
149
+ // Write Bleno into this class
150
+ Bleno.on("stateChange", state => {
151
+ if (state === this.state) return;
152
+ this.state = state;
153
+ logger.debug(`stateChange: ${state}, address = ${Bleno.address}`);
154
+ if (state !== "poweredOn") {
155
+ Bleno.stopAdvertising();
156
+ } else if (this.advertisingData) {
157
+ Bleno.startAdvertisingWithEIRData(this.advertisingData);
158
+ this.isAdvertising = true;
159
+ }
160
+ });
161
+
162
+ // Linux only events /////////////////
163
+ Bleno.on("accept", clientAddress => {
164
+ logger.debug(`accept new connection, client: ${clientAddress}`);
165
+ this.clientAddress = clientAddress;
166
+ Bleno.updateRssi();
167
+ });
168
+
169
+ Bleno.on("disconnect", clientAddress => {
170
+ logger.debug(`disconnect, client: ${clientAddress}`);
171
+ if (this.btpSession !== undefined) {
172
+ this.btpSession
173
+ .close()
174
+ .then(() => {
175
+ this.btpSession = undefined;
176
+ })
177
+ .catch(() => {
178
+ this.btpSession = undefined;
179
+ });
180
+ }
181
+ });
182
+
183
+ Bleno.on("rssiUpdate", rssi => {
184
+ logger.debug(`rssiUpdate: ${rssi}`);
185
+ });
186
+ //////////////////////////////////////
187
+
188
+ Bleno.on("mtuChange", mtu => {
189
+ logger.debug(`mtuChange: ${mtu}`);
190
+ });
191
+
192
+ Bleno.on("advertisingStart", error => {
193
+ logger.debug(`advertisingStart: ${error ? `error ${error}` : "success"}`);
194
+
195
+ if (!error) {
196
+ Bleno.setServices([this.matterBleService]);
197
+ }
198
+ // TODO handle transport error
199
+ });
200
+
201
+ Bleno.on("advertisingStop", () => {
202
+ logger.debug("advertisingStop");
203
+ });
204
+
205
+ Bleno.on("servicesSet", error => {
206
+ logger.debug(`servicesSet: ${error ? `error ${error}` : "success"}`);
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Process a Write request on characteristic C1 from the Matter service.
212
+ * The data are checked if it might be a handshake request and stored until the subscribe request comes in.
213
+ * Otherwise, the data are forwarded to the BTP session handler to be decoded and processed.
214
+ *
215
+ * @param data
216
+ * @param offset
217
+ * @param withoutResponse
218
+ */
219
+ handleC1WriteRequest(data: Buffer, offset: number, withoutResponse: boolean) {
220
+ if (offset !== 0 || withoutResponse) {
221
+ throw new BleError(`Offset ${offset} or withoutResponse ${withoutResponse} not supported`);
222
+ }
223
+
224
+ if (data[0] === 0x65 && data[1] === 0x6c && data.length === 9) {
225
+ // Check if the first two bytes and length match the Matter handshake
226
+ this.btpHandshakeTimeout.start(); // starts timer
227
+
228
+ logger.info(
229
+ `Received Matter handshake request: ${data.toString("hex")}, store until subscribe request comes in.`,
230
+ );
231
+ this.latestHandshakePayload = data;
232
+ // TODO Handle edge case where handshake comes with an already open BTP session (should never happen?)
233
+ } else {
234
+ if (this.btpSession !== undefined) {
235
+ logger.debug(`Received Matter data for BTP Session: ${data.toString("hex")}`);
236
+ void this.btpSession.handleIncomingBleData(new Uint8Array(data));
237
+ } else {
238
+ throw new BtpFlowError(
239
+ `Received Matter data but no BTP session was initialized: ${data.toString("hex")}`,
240
+ );
241
+ }
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Process a Subscribe request on characteristic C2 from the Matter service.
247
+ * This is expected directly after a handshake request and initializes the BTP session handler with the stored
248
+ * handshake payload.
249
+ * The BtpSessionHandler instance is wired with the bleno instance for sending data and disconnecting.
250
+ *
251
+ * @param maxValueSize
252
+ * @param updateValueCallback
253
+ */
254
+ async handleC2SubscribeRequest(maxValueSize: number, updateValueCallback: (data: Buffer) => void) {
255
+ if (this.latestHandshakePayload === undefined) {
256
+ throw new BtpFlowError(`Subscription request received before handshake Request`);
257
+ }
258
+ if (this.btpSession !== undefined) {
259
+ throw new BtpFlowError(
260
+ `Subscription request received but BTP session already initialized. Cannot handle two sessions!`,
261
+ );
262
+ }
263
+ this.btpHandshakeTimeout.stop();
264
+
265
+ this.btpSession = await BtpSessionHandler.createFromHandshakeRequest(
266
+ Math.min(Bleno.mtu - 3, maxValueSize),
267
+ new Uint8Array(this.latestHandshakePayload),
268
+
269
+ // callback to write data to characteristic C2
270
+ async (data: Uint8Array) => {
271
+ updateValueCallback(Buffer.from(data.buffer));
272
+ const { promise, resolver } = createPromise<void>();
273
+ this.writeConformationResolver = resolver;
274
+
275
+ return promise;
276
+ },
277
+
278
+ // callback to disconnect the BLE connection
279
+ async () => this.close(),
280
+
281
+ // callback to forward decoded and de-assembled Matter messages to ExchangeManager
282
+ async (data: Uint8Array) => {
283
+ if (this.onMatterMessageListener === undefined) {
284
+ throw new InternalError(`No listener registered for Matter messages`);
285
+ }
286
+ this.onMatterMessageListener(this, data);
287
+ },
288
+ );
289
+ this.latestHandshakePayload = undefined; // BTP Session initialized, handshake payload not needed anymore
290
+ }
291
+
292
+ handleC2Indicate() {
293
+ if (this.writeConformationResolver !== undefined) {
294
+ this.writeConformationResolver();
295
+ this.writeConformationResolver = undefined;
296
+ } else {
297
+ logger.warn(`Received C2 indication but no former write expected a confirmation`);
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Process a Read request on characteristic C3 from the Matter service.
303
+ * The relevant data needs optionally to be set before advertising, else empty data are used.
304
+ *
305
+ * @param offset
306
+ */
307
+ handleC3ReadRequest(offset: number) {
308
+ if (offset > this.additionalAdvertisingData.length) {
309
+ throw new BleError(`Offset ${offset} is larger than data ${this.additionalAdvertisingData.length}`);
310
+ } else {
311
+ return this.additionalAdvertisingData.subarray(offset);
312
+ }
313
+ }
314
+
315
+ async advertise(advertiseData: Uint8Array, additionalAdvertisementData?: Uint8Array) {
316
+ this.advertisingData = Buffer.from(advertiseData.buffer);
317
+
318
+ if (additionalAdvertisementData) {
319
+ this.additionalAdvertisingData = Buffer.from(additionalAdvertisementData.buffer);
320
+ } else {
321
+ this.additionalAdvertisingData = Buffer.alloc(0);
322
+ }
323
+
324
+ if (this.isAdvertising) {
325
+ await this.stopAdvertising();
326
+ this.isAdvertising = false;
327
+ }
328
+
329
+ if (this.state === "poweredOn") {
330
+ Bleno.startAdvertisingWithEIRData(this.advertisingData);
331
+ this.isAdvertising = true;
332
+ } else {
333
+ logger.debug(`State is ${this.state}, advertise when powered on`);
334
+ }
335
+ return new Promise<void>(resolve => {
336
+ Bleno.once("advertisingStart", () => resolve());
337
+ });
338
+ }
339
+
340
+ async stopAdvertising() {
341
+ if (this.isAdvertising) {
342
+ return new Promise<void>(resolve => {
343
+ Bleno.stopAdvertising();
344
+ Bleno.once("advertisingStop", () => {
345
+ this.isAdvertising = false;
346
+ resolve();
347
+ });
348
+ });
349
+ }
350
+ }
351
+
352
+ setMatterMessageListener(listener: (socket: Channel<Uint8Array>, data: Uint8Array) => void) {
353
+ if (this.onMatterMessageListener !== undefined) {
354
+ throw new InternalError(`onData listener already set`);
355
+ }
356
+ this.onMatterMessageListener = listener;
357
+ }
358
+
359
+ async btpHandshakeTimeoutTriggered() {
360
+ await this.disconnect();
361
+ logger.error("Timeout for handshake subscribe request on C2 reached, disconnecting.");
362
+ }
363
+
364
+ async close() {
365
+ this.btpHandshakeTimeout.stop();
366
+ await this.disconnect();
367
+ if (this.btpSession !== undefined) {
368
+ await this.btpSession.close();
369
+ this.btpSession = undefined;
370
+ }
371
+ this.onMatterMessageListener = undefined;
372
+ }
373
+
374
+ async disconnect() {
375
+ Bleno.disconnect();
376
+ /*
377
+ TODO: This is not working as expected, the disconnect event is not triggered, seems issue in Bleno
378
+ return new Promise<void>(resolve => {
379
+ Bleno.once("disconnect", () => {
380
+ console.log("DISCONNECTED");
381
+ resolve();
382
+ });
383
+ });*/
384
+ }
385
+
386
+ // Channel<Uint8Array>
387
+ /**
388
+ * Send a Matter message to the connected device - need to do BTP assembly first.
389
+ *
390
+ * @param data
391
+ */
392
+ async send(data: Uint8Array) {
393
+ if (this.btpSession === undefined) {
394
+ throw new ChannelNotConnectedError(`Cannot send data, no BTP session initialized`);
395
+ }
396
+ await this.btpSession.sendMatterMessage(data);
397
+ }
398
+
399
+ // Channel<Uint8Array>
400
+ get name() {
401
+ return `${this.type}://${this.clientAddress}`;
402
+ }
403
+ }