@matter/react-native 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 (161) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +56 -0
  3. package/dist/cjs/ble/BleReactNative.d.ts +17 -0
  4. package/dist/cjs/ble/BleReactNative.d.ts.map +1 -0
  5. package/dist/cjs/ble/BleReactNative.js +61 -0
  6. package/dist/cjs/ble/BleReactNative.js.map +6 -0
  7. package/dist/cjs/ble/BleScanner.d.ts +52 -0
  8. package/dist/cjs/ble/BleScanner.d.ts.map +1 -0
  9. package/dist/cjs/ble/BleScanner.js +239 -0
  10. package/dist/cjs/ble/BleScanner.js.map +6 -0
  11. package/dist/cjs/ble/ReactNativeBleChannel.d.ts +33 -0
  12. package/dist/cjs/ble/ReactNativeBleChannel.d.ts.map +1 -0
  13. package/dist/cjs/ble/ReactNativeBleChannel.js +252 -0
  14. package/dist/cjs/ble/ReactNativeBleChannel.js.map +6 -0
  15. package/dist/cjs/ble/ReactNativeBleClient.d.ts +25 -0
  16. package/dist/cjs/ble/ReactNativeBleClient.d.ts.map +1 -0
  17. package/dist/cjs/ble/ReactNativeBleClient.js +143 -0
  18. package/dist/cjs/ble/ReactNativeBleClient.js.map +6 -0
  19. package/dist/cjs/ble/export.d.ts +8 -0
  20. package/dist/cjs/ble/export.d.ts.map +1 -0
  21. package/dist/cjs/ble/export.js +25 -0
  22. package/dist/cjs/ble/export.js.map +6 -0
  23. package/dist/cjs/crypto/ReactNativeCrypto.d.ts +12 -0
  24. package/dist/cjs/crypto/ReactNativeCrypto.d.ts.map +1 -0
  25. package/dist/cjs/crypto/ReactNativeCrypto.js +95 -0
  26. package/dist/cjs/crypto/ReactNativeCrypto.js.map +6 -0
  27. package/dist/cjs/crypto/export.d.ts +7 -0
  28. package/dist/cjs/crypto/export.d.ts.map +1 -0
  29. package/dist/cjs/crypto/export.js +24 -0
  30. package/dist/cjs/crypto/export.js.map +6 -0
  31. package/dist/cjs/crypto/register.d.ts +7 -0
  32. package/dist/cjs/crypto/register.d.ts.map +1 -0
  33. package/dist/cjs/crypto/register.js +15 -0
  34. package/dist/cjs/crypto/register.js.map +6 -0
  35. package/dist/cjs/environment/ReactNativeEnvironment.d.ts +16 -0
  36. package/dist/cjs/environment/ReactNativeEnvironment.d.ts.map +1 -0
  37. package/dist/cjs/environment/ReactNativeEnvironment.js +60 -0
  38. package/dist/cjs/environment/ReactNativeEnvironment.js.map +6 -0
  39. package/dist/cjs/environment/export.d.ts +7 -0
  40. package/dist/cjs/environment/export.d.ts.map +1 -0
  41. package/dist/cjs/environment/export.js +24 -0
  42. package/dist/cjs/environment/export.js.map +6 -0
  43. package/dist/cjs/environment/register.d.ts +7 -0
  44. package/dist/cjs/environment/register.d.ts.map +1 -0
  45. package/dist/cjs/environment/register.js +12 -0
  46. package/dist/cjs/environment/register.js.map +6 -0
  47. package/dist/cjs/export.d.ts +9 -0
  48. package/dist/cjs/export.d.ts.map +1 -0
  49. package/dist/cjs/export.js +10 -0
  50. package/dist/cjs/export.js.map +6 -0
  51. package/dist/cjs/net/NetworkReactNative.d.ts +20 -0
  52. package/dist/cjs/net/NetworkReactNative.d.ts.map +1 -0
  53. package/dist/cjs/net/NetworkReactNative.js +173 -0
  54. package/dist/cjs/net/NetworkReactNative.js.map +6 -0
  55. package/dist/cjs/net/UdpChannelReactNative.d.ts +39 -0
  56. package/dist/cjs/net/UdpChannelReactNative.d.ts.map +1 -0
  57. package/dist/cjs/net/UdpChannelReactNative.js +184 -0
  58. package/dist/cjs/net/UdpChannelReactNative.js.map +6 -0
  59. package/dist/cjs/net/export.d.ts +8 -0
  60. package/dist/cjs/net/export.d.ts.map +1 -0
  61. package/dist/cjs/net/export.js +35 -0
  62. package/dist/cjs/net/export.js.map +6 -0
  63. package/dist/cjs/net/register.d.ts +7 -0
  64. package/dist/cjs/net/register.d.ts.map +1 -0
  65. package/dist/cjs/net/register.js +15 -0
  66. package/dist/cjs/net/register.js.map +6 -0
  67. package/dist/cjs/package.json +10 -0
  68. package/dist/cjs/storage/StorageBackendAsyncStorage.d.ts +31 -0
  69. package/dist/cjs/storage/StorageBackendAsyncStorage.d.ts.map +1 -0
  70. package/dist/cjs/storage/StorageBackendAsyncStorage.js +142 -0
  71. package/dist/cjs/storage/StorageBackendAsyncStorage.js.map +6 -0
  72. package/dist/cjs/tsconfig.tsbuildinfo +1 -0
  73. package/dist/esm/ble/BleReactNative.d.ts +17 -0
  74. package/dist/esm/ble/BleReactNative.d.ts.map +1 -0
  75. package/dist/esm/ble/BleReactNative.js +41 -0
  76. package/dist/esm/ble/BleReactNative.js.map +6 -0
  77. package/dist/esm/ble/BleScanner.d.ts +52 -0
  78. package/dist/esm/ble/BleScanner.d.ts.map +1 -0
  79. package/dist/esm/ble/BleScanner.js +219 -0
  80. package/dist/esm/ble/BleScanner.js.map +6 -0
  81. package/dist/esm/ble/ReactNativeBleChannel.d.ts +33 -0
  82. package/dist/esm/ble/ReactNativeBleChannel.d.ts.map +1 -0
  83. package/dist/esm/ble/ReactNativeBleChannel.js +257 -0
  84. package/dist/esm/ble/ReactNativeBleChannel.js.map +6 -0
  85. package/dist/esm/ble/ReactNativeBleClient.d.ts +25 -0
  86. package/dist/esm/ble/ReactNativeBleClient.d.ts.map +1 -0
  87. package/dist/esm/ble/ReactNativeBleClient.js +123 -0
  88. package/dist/esm/ble/ReactNativeBleClient.js.map +6 -0
  89. package/dist/esm/ble/export.d.ts +8 -0
  90. package/dist/esm/ble/export.d.ts.map +1 -0
  91. package/dist/esm/ble/export.js +8 -0
  92. package/dist/esm/ble/export.js.map +6 -0
  93. package/dist/esm/crypto/ReactNativeCrypto.d.ts +12 -0
  94. package/dist/esm/crypto/ReactNativeCrypto.d.ts.map +1 -0
  95. package/dist/esm/crypto/ReactNativeCrypto.js +65 -0
  96. package/dist/esm/crypto/ReactNativeCrypto.js.map +6 -0
  97. package/dist/esm/crypto/export.d.ts +7 -0
  98. package/dist/esm/crypto/export.d.ts.map +1 -0
  99. package/dist/esm/crypto/export.js +7 -0
  100. package/dist/esm/crypto/export.js.map +6 -0
  101. package/dist/esm/crypto/register.d.ts +7 -0
  102. package/dist/esm/crypto/register.d.ts.map +1 -0
  103. package/dist/esm/crypto/register.js +14 -0
  104. package/dist/esm/crypto/register.js.map +6 -0
  105. package/dist/esm/environment/ReactNativeEnvironment.d.ts +16 -0
  106. package/dist/esm/environment/ReactNativeEnvironment.d.ts.map +1 -0
  107. package/dist/esm/environment/ReactNativeEnvironment.js +40 -0
  108. package/dist/esm/environment/ReactNativeEnvironment.js.map +6 -0
  109. package/dist/esm/environment/export.d.ts +7 -0
  110. package/dist/esm/environment/export.d.ts.map +1 -0
  111. package/dist/esm/environment/export.js +7 -0
  112. package/dist/esm/environment/export.js.map +6 -0
  113. package/dist/esm/environment/register.d.ts +7 -0
  114. package/dist/esm/environment/register.d.ts.map +1 -0
  115. package/dist/esm/environment/register.js +11 -0
  116. package/dist/esm/environment/register.js.map +6 -0
  117. package/dist/esm/export.d.ts +9 -0
  118. package/dist/esm/export.d.ts.map +1 -0
  119. package/dist/esm/export.js +9 -0
  120. package/dist/esm/export.js.map +6 -0
  121. package/dist/esm/net/NetworkReactNative.d.ts +20 -0
  122. package/dist/esm/net/NetworkReactNative.d.ts.map +1 -0
  123. package/dist/esm/net/NetworkReactNative.js +151 -0
  124. package/dist/esm/net/NetworkReactNative.js.map +6 -0
  125. package/dist/esm/net/UdpChannelReactNative.d.ts +39 -0
  126. package/dist/esm/net/UdpChannelReactNative.d.ts.map +1 -0
  127. package/dist/esm/net/UdpChannelReactNative.js +162 -0
  128. package/dist/esm/net/UdpChannelReactNative.js.map +6 -0
  129. package/dist/esm/net/export.d.ts +8 -0
  130. package/dist/esm/net/export.d.ts.map +1 -0
  131. package/dist/esm/net/export.js +14 -0
  132. package/dist/esm/net/export.js.map +6 -0
  133. package/dist/esm/net/register.d.ts +7 -0
  134. package/dist/esm/net/register.d.ts.map +1 -0
  135. package/dist/esm/net/register.js +14 -0
  136. package/dist/esm/net/register.js.map +6 -0
  137. package/dist/esm/package.json +10 -0
  138. package/dist/esm/storage/StorageBackendAsyncStorage.d.ts +31 -0
  139. package/dist/esm/storage/StorageBackendAsyncStorage.d.ts.map +1 -0
  140. package/dist/esm/storage/StorageBackendAsyncStorage.js +112 -0
  141. package/dist/esm/storage/StorageBackendAsyncStorage.js.map +6 -0
  142. package/dist/esm/tsconfig.tsbuildinfo +1 -0
  143. package/package.json +105 -0
  144. package/src/ble/BleReactNative.ts +45 -0
  145. package/src/ble/BleScanner.ts +277 -0
  146. package/src/ble/ReactNativeBleChannel.ts +313 -0
  147. package/src/ble/ReactNativeBleClient.ts +132 -0
  148. package/src/ble/export.ts +8 -0
  149. package/src/crypto/ReactNativeCrypto.ts +108 -0
  150. package/src/crypto/export.ts +7 -0
  151. package/src/crypto/register.ts +16 -0
  152. package/src/environment/ReactNativeEnvironment.ts +53 -0
  153. package/src/environment/export.ts +7 -0
  154. package/src/environment/register.ts +16 -0
  155. package/src/export.ts +11 -0
  156. package/src/net/NetworkReactNative.ts +190 -0
  157. package/src/net/UdpChannelReactNative.ts +219 -0
  158. package/src/net/export.ts +13 -0
  159. package/src/net/register.ts +17 -0
  160. package/src/storage/StorageBackendAsyncStorage.ts +145 -0
  161. package/src/tsconfig.json +17 -0
@@ -0,0 +1,313 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2024 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import {
8
+ Bytes,
9
+ Channel,
10
+ ChannelType,
11
+ InternalError,
12
+ Logger,
13
+ NetInterface,
14
+ ServerAddress,
15
+ Time,
16
+ TransportInterface,
17
+ createPromise,
18
+ } from "#general";
19
+ import {
20
+ BLE_MATTER_C1_CHARACTERISTIC_UUID,
21
+ BLE_MATTER_C2_CHARACTERISTIC_UUID,
22
+ BLE_MATTER_C3_CHARACTERISTIC_UUID,
23
+ BLE_MATTER_SERVICE_UUID,
24
+ BLE_MAXIMUM_BTP_MTU,
25
+ BTP_CONN_RSP_TIMEOUT_MS,
26
+ BTP_MAXIMUM_WINDOW_SIZE,
27
+ BTP_SUPPORTED_VERSIONS,
28
+ Ble,
29
+ BleChannel,
30
+ BleError,
31
+ BtpCodec,
32
+ BtpFlowError,
33
+ BtpSessionHandler,
34
+ } from "#protocol";
35
+ import {
36
+ BleErrorCode,
37
+ Characteristic,
38
+ Device,
39
+ BleError as ReactNativeBleError,
40
+ Subscription,
41
+ } from "react-native-ble-plx";
42
+ import { BleScanner } from "./BleScanner.js";
43
+
44
+ const logger = Logger.get("BleChannel");
45
+
46
+ export class ReactNativeBleCentralInterface implements NetInterface {
47
+ private openChannels: Map<ServerAddress, Device> = new Map();
48
+ private onMatterMessageListener: ((socket: Channel<Uint8Array>, data: Uint8Array) => void) | undefined;
49
+
50
+ async openChannel(address: ServerAddress): Promise<Channel<Uint8Array>> {
51
+ if (address.type !== "ble") {
52
+ throw new InternalError(`Unsupported address type ${address.type}.`);
53
+ }
54
+ if (this.onMatterMessageListener === undefined) {
55
+ throw new InternalError(`Network Interface was not added to the system yet.`);
56
+ }
57
+
58
+ // Get the peripheral by address and connect to it.
59
+ const { peripheral, hasAdditionalAdvertisementData } = (
60
+ Ble.get().getBleScanner() as BleScanner
61
+ ).getDiscoveredDevice(address.peripheralAddress);
62
+ if (this.openChannels.has(address)) {
63
+ throw new BleError(
64
+ `Peripheral ${address.peripheralAddress} is already connected. Only one connection supported right now.`,
65
+ );
66
+ }
67
+ logger.debug(`Connect to Peripheral now`);
68
+ let device: Device;
69
+ try {
70
+ device = await peripheral.connect();
71
+ } catch (error) {
72
+ if (error instanceof ReactNativeBleError && error.errorCode === BleErrorCode.DeviceAlreadyConnected) {
73
+ device = peripheral;
74
+ } else {
75
+ throw new BleError(`Error connecting to peripheral: ${(error as any).message}`);
76
+ }
77
+ }
78
+ logger.debug(`Peripheral connected successfully, MTU = ${device.mtu}`);
79
+
80
+ // Once the peripheral has been connected, then discover the services and characteristics of interest.
81
+ device = await device.discoverAllServicesAndCharacteristics();
82
+
83
+ const services = await device.services();
84
+
85
+ for (const service of services) {
86
+ logger.debug(`found service: ${service.uuid}`);
87
+ if (service.uuid !== BLE_MATTER_SERVICE_UUID) continue;
88
+
89
+ // So, discover its characteristics.
90
+ const characteristics = await device.characteristicsForService(service.uuid);
91
+
92
+ let characteristicC1ForWrite: Characteristic | undefined;
93
+ let characteristicC2ForSubscribe: Characteristic | undefined;
94
+ let additionalCommissioningRelatedData: Uint8Array | undefined;
95
+
96
+ for (const characteristic of characteristics) {
97
+ // Loop through each characteristic and match them to the UUIDs that we know about.
98
+ logger.debug("found characteristic:", characteristic.uuid);
99
+
100
+ switch (characteristic.uuid) {
101
+ case BLE_MATTER_C1_CHARACTERISTIC_UUID:
102
+ logger.debug("found C1 characteristic");
103
+ characteristicC1ForWrite = characteristic;
104
+ break;
105
+
106
+ case BLE_MATTER_C2_CHARACTERISTIC_UUID:
107
+ logger.debug("found C2 characteristic");
108
+ characteristicC2ForSubscribe = characteristic;
109
+ break;
110
+
111
+ case BLE_MATTER_C3_CHARACTERISTIC_UUID:
112
+ logger.debug("found C3 characteristic");
113
+ if (hasAdditionalAdvertisementData) {
114
+ logger.debug("reading additional commissioning related data");
115
+ const characteristicWithValue = await service.readCharacteristic(characteristic.uuid);
116
+ if (characteristicWithValue.value !== null) {
117
+ additionalCommissioningRelatedData = Bytes.fromBase64(characteristicWithValue.value);
118
+ } else {
119
+ logger.debug("no value in characteristic C3");
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ if (!characteristicC1ForWrite || !characteristicC2ForSubscribe) {
126
+ logger.debug("missing characteristics");
127
+ continue;
128
+ }
129
+
130
+ this.openChannels.set(address, peripheral);
131
+ return await ReactNativeBleChannel.create(
132
+ peripheral,
133
+ characteristicC1ForWrite,
134
+ characteristicC2ForSubscribe,
135
+ this.onMatterMessageListener,
136
+ additionalCommissioningRelatedData,
137
+ );
138
+ }
139
+
140
+ throw new BleError(`No Matter service found on peripheral ${peripheral.id}`);
141
+ }
142
+
143
+ onData(listener: (socket: Channel<Uint8Array>, data: Uint8Array) => void): TransportInterface.Listener {
144
+ this.onMatterMessageListener = listener;
145
+ return {
146
+ close: async () => await this.close(),
147
+ };
148
+ }
149
+
150
+ async close() {
151
+ for (const peripheral of this.openChannels.values()) {
152
+ await peripheral.cancelConnection();
153
+ }
154
+ }
155
+
156
+ supports(type: ChannelType) {
157
+ return type === ChannelType.BLE;
158
+ }
159
+ }
160
+
161
+ export class ReactNativeBleChannel extends BleChannel<Uint8Array> {
162
+ static async create(
163
+ peripheral: Device,
164
+ characteristicC1ForWrite: Characteristic,
165
+ characteristicC2ForSubscribe: Characteristic,
166
+ onMatterMessageListener: (socket: Channel<Uint8Array>, data: Uint8Array) => void,
167
+ _additionalCommissioningRelatedData?: Uint8Array,
168
+ ): Promise<ReactNativeBleChannel> {
169
+ let mtu = peripheral.mtu ?? 0;
170
+ if (mtu > BLE_MAXIMUM_BTP_MTU) {
171
+ mtu = BLE_MAXIMUM_BTP_MTU;
172
+ }
173
+ logger.debug(`Using MTU=${mtu} (Peripheral MTU=${peripheral.mtu})`);
174
+ const btpHandshakeRequest = BtpCodec.encodeBtpHandshakeRequest({
175
+ versions: BTP_SUPPORTED_VERSIONS,
176
+ attMtu: mtu,
177
+ clientWindowSize: BTP_MAXIMUM_WINDOW_SIZE,
178
+ });
179
+ logger.debug(`sending BTP handshake request: ${Logger.toJSON(btpHandshakeRequest)}`);
180
+ characteristicC1ForWrite = await characteristicC1ForWrite.writeWithResponse(
181
+ Bytes.toBase64(btpHandshakeRequest),
182
+ );
183
+
184
+ const btpHandshakeTimeout = Time.getTimer("BLE handshake timeout", BTP_CONN_RSP_TIMEOUT_MS, async () => {
185
+ await peripheral.cancelConnection();
186
+ logger.debug("Handshake Response not received. Disconnected from peripheral");
187
+ }).start();
188
+
189
+ logger.debug("subscribing to C2 characteristic");
190
+
191
+ const { promise: handshakeResponseReceivedPromise, resolver } = createPromise<Uint8Array>();
192
+
193
+ let handshakeReceived = false;
194
+ const handshakeSubscription = characteristicC2ForSubscribe.monitor((error, characteristic) => {
195
+ if (error !== null || characteristic === null) {
196
+ if (error instanceof ReactNativeBleError && error.errorCode === 2 && handshakeReceived) {
197
+ // Subscription got removed after handshake was received, all good
198
+ return;
199
+ }
200
+ logger.debug("Error while monitoring C2 characteristic", error);
201
+ return;
202
+ }
203
+ const characteristicData = characteristic.value;
204
+ if (characteristicData === null) {
205
+ logger.debug("C2 characteristic value is null");
206
+ return;
207
+ }
208
+ const data = Bytes.fromBase64(characteristicData);
209
+ logger.debug(`received first data on C2: ${Bytes.toHex(data)}`);
210
+
211
+ if (data[0] === 0x65 && data[1] === 0x6c && data.length === 6) {
212
+ // Check if the first two bytes and length match the Matter handshake
213
+ logger.info(`Received Matter handshake response: ${Bytes.toHex(data)}.`);
214
+ btpHandshakeTimeout.stop();
215
+ resolver(data);
216
+ }
217
+ });
218
+
219
+ const handshakeResponse = await handshakeResponseReceivedPromise;
220
+ handshakeReceived = true;
221
+ handshakeSubscription.remove();
222
+
223
+ let connectionCloseExpected = false;
224
+ const btpSession = await BtpSessionHandler.createAsCentral(
225
+ new Uint8Array(handshakeResponse),
226
+ // callback to write data to characteristic C1
227
+ async data => {
228
+ characteristicC1ForWrite = await characteristicC1ForWrite.writeWithResponse(Bytes.toBase64(data));
229
+ },
230
+ // callback to disconnect the BLE connection
231
+ async () => {
232
+ connectionCloseExpected = true;
233
+ dataSubscription.remove();
234
+ await peripheral.cancelConnection();
235
+ logger.debug("disconnected from peripheral");
236
+ },
237
+
238
+ // callback to forward decoded and de-assembled Matter messages to ExchangeManager
239
+ async data => {
240
+ if (onMatterMessageListener === undefined) {
241
+ throw new InternalError(`No listener registered for Matter messages`);
242
+ }
243
+ onMatterMessageListener(bleChannel, data);
244
+ },
245
+ );
246
+
247
+ const dataSubscription = characteristicC2ForSubscribe.monitor((error, characteristic) => {
248
+ if (error !== null || characteristic === null) {
249
+ if (error instanceof ReactNativeBleError && error.errorCode === 2 && connectionCloseExpected) {
250
+ // Subscription got removed and received, all good
251
+ return;
252
+ }
253
+ logger.debug("Error while monitoring C2 characteristic", error);
254
+ return;
255
+ }
256
+ const characteristicData = characteristic.value;
257
+ if (characteristicData === null) {
258
+ logger.debug("C2 characteristic value is null");
259
+ return;
260
+ }
261
+ const data = Bytes.fromBase64(characteristicData);
262
+ logger.debug(`received data on C2: ${Bytes.toHex(data)}`);
263
+
264
+ void btpSession.handleIncomingBleData(new Uint8Array(data));
265
+ });
266
+
267
+ const bleChannel = new ReactNativeBleChannel(peripheral, btpSession);
268
+ return bleChannel;
269
+ }
270
+
271
+ private connected = true;
272
+ private disconnectSubscription: Subscription;
273
+
274
+ constructor(
275
+ private readonly peripheral: Device,
276
+ private readonly btpSession: BtpSessionHandler,
277
+ ) {
278
+ super();
279
+ this.disconnectSubscription = peripheral.onDisconnected(error => {
280
+ logger.debug(`Disconnected from peripheral ${peripheral.id}: ${error}`);
281
+ this.connected = false;
282
+ this.disconnectSubscription.remove();
283
+ void this.btpSession.close();
284
+ });
285
+ }
286
+
287
+ /**
288
+ * Send a Matter message to the connected device - need to do BTP assembly first.
289
+ *
290
+ * @param data
291
+ */
292
+ async send(data: Uint8Array) {
293
+ if (!this.connected) {
294
+ logger.debug("Cannot send data because not connected to peripheral.");
295
+ return;
296
+ }
297
+ if (this.btpSession === undefined) {
298
+ throw new BtpFlowError(`Cannot send data, no BTP session initialized`);
299
+ }
300
+ await this.btpSession.sendMatterMessage(data);
301
+ }
302
+
303
+ // Channel<Uint8Array>
304
+ get name() {
305
+ return `ble://${this.peripheral.id}`;
306
+ }
307
+
308
+ async close() {
309
+ await this.btpSession.close();
310
+ this.disconnectSubscription.remove();
311
+ await this.peripheral.cancelConnection();
312
+ }
313
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2024 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Bytes, Logger, MatterError } from "#general";
8
+ import { BLE_MATTER_SERVICE_UUID } from "#protocol";
9
+ import { BleError, BleErrorCode, BleManager, State as BluetoothState, Device } from "react-native-ble-plx";
10
+
11
+ const logger = Logger.get("ReactNativeBleClient");
12
+
13
+ export class BluetoothUnauthorizedError extends MatterError {}
14
+ export class BluetoothUnsupportedError extends MatterError {}
15
+
16
+ export class ReactNativeBleClient {
17
+ private readonly bleManager = new BleManager();
18
+ private readonly discoveredPeripherals = new Map<string, { peripheral: Device; matterServiceData: Uint8Array }>();
19
+ private shouldScan = false;
20
+ private isScanning = false;
21
+ private bleState = BluetoothState.Unknown;
22
+ private deviceDiscoveredCallback: ((peripheral: Device, manufacturerData: Uint8Array) => void) | undefined;
23
+
24
+ constructor() {
25
+ // this.bleMnager.setLogLevel(LogLevel.Verbose)
26
+ const subscription = this.bleManager.onStateChange(state => {
27
+ this.bleState = state;
28
+ logger.debug(`BLE state changed to ${state}`);
29
+ switch (state) {
30
+ case BluetoothState.PoweredOff:
31
+ this.bleManager.enable().catch(error => {
32
+ subscription.remove();
33
+ if (error instanceof BleError && error.errorCode === BleErrorCode.BluetoothUnauthorized) {
34
+ throw new BluetoothUnauthorizedError("Bluetooth is unauthorized");
35
+ }
36
+ throw error;
37
+ });
38
+ break;
39
+ case BluetoothState.PoweredOn:
40
+ subscription.remove();
41
+ if (this.shouldScan) {
42
+ void this.startScanning();
43
+ }
44
+ break;
45
+ case BluetoothState.Unauthorized:
46
+ subscription.remove();
47
+ throw new BluetoothUnauthorizedError("Bluetooth is unauthorized");
48
+ case BluetoothState.Unsupported:
49
+ subscription.remove();
50
+ throw new BluetoothUnsupportedError("Bluetooth is unsupported");
51
+ default:
52
+ logger.error("Unexpected BLE state", state);
53
+ subscription.remove();
54
+ void this.stopScanning();
55
+ }
56
+ });
57
+ }
58
+
59
+ public setDiscoveryCallback(callback: (peripheral: Device, manufacturerData: Uint8Array) => void) {
60
+ this.deviceDiscoveredCallback = callback;
61
+ for (const { peripheral, matterServiceData } of this.discoveredPeripherals.values()) {
62
+ this.deviceDiscoveredCallback(peripheral, matterServiceData);
63
+ }
64
+ }
65
+
66
+ public async startScanning() {
67
+ if (this.isScanning) return;
68
+
69
+ this.shouldScan = true;
70
+ if (this.bleState === BluetoothState.PoweredOn) {
71
+ logger.debug("Start BLE scanning for Matter Services ...");
72
+ this.isScanning = true;
73
+ await this.bleManager.startDeviceScan([BLE_MATTER_SERVICE_UUID], {}, (error, peripheral) => {
74
+ if (error !== null || peripheral === null) {
75
+ this.isScanning = false;
76
+ logger.error("Error while scanning for BLE devices", error);
77
+ if (this.shouldScan) {
78
+ this.startScanning().catch(error =>
79
+ logger.error("Error while restarting scanning after error", error),
80
+ );
81
+ } else {
82
+ this.stopScanning().catch(error =>
83
+ logger.error("Error while stopping scanning after error", error),
84
+ );
85
+ }
86
+ return;
87
+ }
88
+ this.handleDiscoveredDevice(peripheral);
89
+ });
90
+ } else {
91
+ logger.debug("ble state is not poweredOn ... delay scanning till poweredOn");
92
+ }
93
+ }
94
+
95
+ public async stopScanning() {
96
+ this.shouldScan = false;
97
+ logger.debug("Stop BLE scanning for Matter Services ...");
98
+ await this.bleManager.stopDeviceScan();
99
+ this.isScanning = false;
100
+ }
101
+
102
+ private handleDiscoveredDevice(peripheral: Device) {
103
+ // The advertisement data contains a name, power level (if available), certain advertised service uuids,
104
+ // as well as manufacturer data.
105
+ // {"localName":"MATTER-3840","serviceData":[{"uuid":"fff6","data":{"type":"Buffer","data":[0,0,15,241,255,1,128,0]}}],"serviceUuids":["fff6"],"solicitationServiceUuids":[],"serviceSolicitationUuids":[]}
106
+ logger.debug(
107
+ `Found peripheral ${peripheral.id} (${peripheral.localName}) with serviceData ${Logger.toJSON(peripheral.serviceData)}`,
108
+ );
109
+
110
+ if (!peripheral.isConnectable) {
111
+ logger.info(`Peripheral ${peripheral.id} is not connectable ... ignoring`);
112
+ return;
113
+ }
114
+ const matterServiceDataBase64 = peripheral.serviceData?.[BLE_MATTER_SERVICE_UUID];
115
+ if (matterServiceDataBase64 === undefined) {
116
+ logger.info(`Peripheral ${peripheral.id} does not advertise Matter Service ... ignoring`);
117
+ return;
118
+ }
119
+ const matterServiceData = Bytes.fromBase64(matterServiceDataBase64);
120
+ if (matterServiceData.length !== 8) {
121
+ logger.info(`Peripheral ${peripheral.id} does not advertise Matter Service ... ignoring`);
122
+ return;
123
+ }
124
+
125
+ this.discoveredPeripherals.set(peripheral.id, {
126
+ peripheral,
127
+ matterServiceData: matterServiceData,
128
+ });
129
+
130
+ this.deviceDiscoveredCallback?.(peripheral, matterServiceData);
131
+ }
132
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2024 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ export * from "./BleReactNative.js";
8
+ export * from "./BleScanner.js";
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2024 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { install } from "react-native-quick-crypto";
8
+
9
+ install(); // Install the react-native crypto module
10
+
11
+ import { CRYPTO_HASH_ALGORITHM, CryptoDsaEncoding, CryptoVerifyError } from "#general";
12
+ import { NodeJsCrypto } from "#nodejs";
13
+ import jwk2pem from "jwk-to-pem";
14
+
15
+ // @ts-expect-error No types but all fine
16
+ crypto.hkdf = (
17
+ digest: "sha1" | "sha256" | "sha384" | "sha512",
18
+ ikm: Uint8Array,
19
+ salt: Uint8Array,
20
+ info: Uint8Array,
21
+ keylen: number,
22
+ ) => {
23
+ const hashlen = parseInt(digest.substr(3), 10) >> 3 || 20;
24
+ const prk = crypto
25
+ // @ts-expect-error No types but all fine
26
+ .createHmac(digest, salt.byteLength ? salt : new Uint8Array(hashlen))
27
+ .update(ikm)
28
+ .digest();
29
+
30
+ // T(0) = empty
31
+ // T(1) = HMAC(PRK, T(0) | info | 0x01)
32
+ // T(2) = HMAC(PRK, T(1) | info | 0x02)
33
+ // T(3) = HMAC(PRK, T(2) | info | 0x03)
34
+ // ...
35
+ // T(N) = HMAC(PRK, T(N-1) | info | N)
36
+
37
+ const N = Math.ceil(keylen / hashlen);
38
+
39
+ // Single T buffer to accomodate T = T(1) | T(2) | T(3) | ... | T(N)
40
+ // with a little extra for info | N during T(N)
41
+ const T = new Uint8Array(hashlen * N + info.byteLength + 1);
42
+ let prev = 0;
43
+ let start = 0;
44
+ for (let c = 1; c <= N; c++) {
45
+ T.set(info, start);
46
+ T[start + info.byteLength] = c;
47
+
48
+ T.set(
49
+ crypto
50
+ // @ts-expect-error No types but all fine
51
+ .createHmac(digest, prk)
52
+ .update(T.subarray(prev, start + info.byteLength + 1))
53
+ .digest(),
54
+ start,
55
+ );
56
+
57
+ prev = start;
58
+ start += hashlen;
59
+ }
60
+
61
+ // OKM, releasing T
62
+ return T.slice(0, keylen);
63
+ };
64
+
65
+ export class CryptoReactNative extends NodeJsCrypto {
66
+ override sign(
67
+ privateKey: JsonWebKey,
68
+ data: Uint8Array | Uint8Array[],
69
+ dsaEncoding: CryptoDsaEncoding = "ieee-p1363",
70
+ ): Uint8Array {
71
+ // @ts-expect-error No types but all fine
72
+ const signer = crypto.createSign(CRYPTO_HASH_ALGORITHM);
73
+ if (Array.isArray(data)) {
74
+ data.forEach(chunk => signer.update(chunk));
75
+ } else {
76
+ signer.update(data);
77
+ }
78
+ return new Uint8Array(
79
+ signer.sign({
80
+ key: jwk2pem(privateKey as jwk2pem.JWK, { private: true }),
81
+ format: "pem",
82
+ type: "pkcs8",
83
+ dsaEncoding,
84
+ }),
85
+ );
86
+ }
87
+
88
+ override verify(
89
+ publicKey: JsonWebKey,
90
+ data: Uint8Array,
91
+ signature: Uint8Array,
92
+ dsaEncoding: CryptoDsaEncoding = "ieee-p1363",
93
+ ) {
94
+ // @ts-expect-error No types but all fine
95
+ const verifier = crypto.createVerify(CRYPTO_HASH_ALGORITHM);
96
+ verifier.update(data);
97
+ const success = verifier.verify(
98
+ {
99
+ key: jwk2pem(publicKey as jwk2pem.JWK),
100
+ format: "pem",
101
+ type: "spki",
102
+ dsaEncoding,
103
+ },
104
+ signature,
105
+ );
106
+ if (!success) throw new CryptoVerifyError("Signature verification failed");
107
+ }
108
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2024 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ export * from "./ReactNativeCrypto.js";
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2024 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Crypto, NoProviderError, singleton } from "@matter/general";
8
+ import { CryptoReactNative } from "./ReactNativeCrypto.js";
9
+
10
+ // Check if Crypto singleton is already registered and auto register if not
11
+ try {
12
+ Crypto.get();
13
+ } catch (error) {
14
+ NoProviderError.accept(error);
15
+ Crypto.get = singleton(() => new CryptoReactNative());
16
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2024 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Environment, Network, StorageService, VariableService } from "#general";
8
+ import { NetworkReactNative } from "../net/NetworkReactNative.js";
9
+ import { StorageBackendAsyncStorage } from "../storage/StorageBackendAsyncStorage.js";
10
+
11
+ /**
12
+ * This is the default environment implementation for React-native:
13
+ *
14
+ * - Uses AsyncStorage for storage
15
+ */
16
+ export function ReactNativeEnvironment() {
17
+ const env = new Environment("default");
18
+
19
+ loadVariables(env);
20
+ configureStorage(env);
21
+ configureNetwork(env);
22
+
23
+ return env;
24
+ }
25
+
26
+ function loadVariables(env: Environment) {
27
+ const vars = env.vars;
28
+
29
+ // Install defaults
30
+ vars.addConfigStyle(getDefaults(vars));
31
+ }
32
+
33
+ function configureStorage(env: Environment) {
34
+ const service = env.get(StorageService);
35
+
36
+ env.vars.use(() => {
37
+ service.location = "Memory";
38
+ });
39
+
40
+ service.factory = namespace => new StorageBackendAsyncStorage(namespace);
41
+ }
42
+
43
+ function configureNetwork(env: Environment) {
44
+ env.set(Network, new NetworkReactNative());
45
+ }
46
+
47
+ export function getDefaults(vars: VariableService) {
48
+ const envName = vars.get("environment", "default");
49
+
50
+ return {
51
+ environment: envName,
52
+ };
53
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2024 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ export * from "./ReactNativeEnvironment.js";
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2024 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Environment } from "@project-chip/matter.js/environment";
8
+ import { ReactNativeEnvironment } from "./ReactNativeEnvironment.js";
9
+
10
+ // Mainly import TextEncoder and TextDecoder polyfill
11
+ // @ts-expect-error No types but all fine
12
+ import { polyfill } from "react-native-polyfill-globals/src/encoding";
13
+
14
+ polyfill();
15
+
16
+ Environment.default = ReactNativeEnvironment();
package/src/export.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2024 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import "./net/register.js";
8
+
9
+ import "./crypto/register.js";
10
+
11
+ import "./environment/register.js";