react-native-ble-nitro 1.11.0 → 1.12.0

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 (31) hide show
  1. package/android/src/main/java/com/margelo/nitro/co/zyke/ble/BleNitroBleManager.kt +58 -21
  2. package/ios/BleNitroBleManager.swift +60 -3
  3. package/ios/BlePeripheralDelegate.swift +68 -9
  4. package/lib/commonjs/index.d.ts +1 -1
  5. package/lib/commonjs/index.d.ts.map +1 -1
  6. package/lib/commonjs/index.js +2 -1
  7. package/lib/commonjs/index.js.map +1 -1
  8. package/lib/commonjs/manager.d.ts +17 -1
  9. package/lib/commonjs/manager.d.ts.map +1 -1
  10. package/lib/commonjs/manager.js +54 -9
  11. package/lib/commonjs/manager.js.map +1 -1
  12. package/lib/commonjs/specs/NativeBleNitro.nitro.d.ts +3 -0
  13. package/lib/commonjs/specs/NativeBleNitro.nitro.d.ts.map +1 -1
  14. package/lib/index.d.ts +1 -1
  15. package/lib/index.js +1 -1
  16. package/lib/manager.d.ts +17 -1
  17. package/lib/manager.js +52 -8
  18. package/lib/specs/NativeBleNitro.nitro.d.ts +3 -0
  19. package/nitrogen/generated/android/c++/JHybridNativeBleNitroSpec.cpp +9 -0
  20. package/nitrogen/generated/android/c++/JHybridNativeBleNitroSpec.hpp +2 -0
  21. package/nitrogen/generated/android/kotlin/com/margelo/nitro/co/zyke/ble/HybridNativeBleNitroSpec.kt +13 -0
  22. package/nitrogen/generated/ios/c++/HybridNativeBleNitroSpecSwift.hpp +14 -0
  23. package/nitrogen/generated/ios/swift/HybridNativeBleNitroSpec.swift +2 -0
  24. package/nitrogen/generated/ios/swift/HybridNativeBleNitroSpec_cxx.swift +28 -0
  25. package/nitrogen/generated/shared/c++/HybridNativeBleNitroSpec.cpp +2 -0
  26. package/nitrogen/generated/shared/c++/HybridNativeBleNitroSpec.hpp +2 -0
  27. package/package.json +1 -1
  28. package/src/__tests__/index.test.ts +127 -0
  29. package/src/index.ts +1 -0
  30. package/src/manager.ts +81 -9
  31. package/src/specs/NativeBleNitro.nitro.ts +3 -0
@@ -1 +1 @@
1
- {"version":3,"file":"NativeBleNitro.nitro.d.ts","sourceRoot":"","sources":["../../../src/specs/NativeBleNitro.nitro.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"NativeBleNitro.nitro.d.ts","sourceRoot":"","sources":["../../../src/specs/NativeBleNitro.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAG1D,MAAM,MAAM,QAAQ,GAAG,WAAW,CAAC;AAGnC,oBAAY,QAAQ;IAClB,OAAO,IAAI;IACX,SAAS,IAAI;IACb,WAAW,IAAI;IACf,YAAY,IAAI;IAChB,UAAU,IAAI;IACd,SAAS,IAAI;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,kBAAkB,EAAE,qBAAqB,EAAE,CAAC;CAC7C;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,OAAO,CAAC;IACvB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,oBAAY,eAAe;IACzB,UAAU,IAAI;IACd,QAAQ,IAAI;IACZ,QAAQ,IAAI;IACZ,aAAa,IAAI;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,EAAE,eAAe,CAAC;CAClC;AAED,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;AACpF,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,KAAK,IAAI,CAAC;AAC7D,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;AAC7F,MAAM,MAAM,0BAA0B,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;AACzG,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;AAC1E,MAAM,MAAM,sBAAsB,GAAG,CAAC,gBAAgB,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;AACxF,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;AACtD,MAAM,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;AACxD,MAAM,MAAM,mBAAmB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;AAC7D,MAAM,MAAM,0BAA0B,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;AACnG,MAAM,MAAM,2BAA2B,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;AAC5G,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;AACvF,MAAM,MAAM,eAAe,GAAG,CAAC,mBAAmB,EAAE,SAAS,EAAE,KAAK,IAAI,CAAC;AAEzE,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,cAAe,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IAEvF,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,uBAAuB,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;IACzD,WAAW,IAAI,IAAI,CAAC;IAGpB,SAAS,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IAC5D,QAAQ,IAAI,OAAO,CAAC;IACpB,UAAU,IAAI,OAAO,CAAC;IAGtB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,CAAC;IAGrD,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,EAAE,kBAAkB,CAAC,EAAE,0BAA0B,EAAE,kBAAkB,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC7I,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAChE,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IACvC,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAClD,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAG7D,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACtE,gGAAgG;IAChG,mCAAmC,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACzF,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACxC,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAGlE,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,QAAQ,EAAE,0BAA0B,GAAG,IAAI,CAAC;IAC9H,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,2BAA2B,GAAG,IAAI,CAAC;IACvK,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,cAAc,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAC9K,6BAA6B,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAChI,4BAA4B,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC;IAGrG,sBAAsB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAC1D,KAAK,IAAI,QAAQ,CAAC;IAClB,sBAAsB,CAAC,aAAa,EAAE,aAAa,GAAG,eAAe,CAAC;IACtE,0BAA0B,IAAI,eAAe,CAAC;IAC9C,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B"}
package/lib/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { type ByteArray, type ScanFilter, type BLEDevice, type ScanCallback, type ManufacturerDataEntry, type ManufacturerData, type ConnectionCallback, type DisconnectEventCallback, type OperationCallback, type CharacteristicUpdateCallback, type Subscription, type AsyncSubscription, type BleNitroManagerOptions, BLEState, AndroidScanMode, BleNitroManager, } from "./manager";
1
+ export { type ByteArray, type ScanFilter, type BLEDevice, type ScanCallback, type ManufacturerDataEntry, type ManufacturerData, type ConnectionCallback, type DisconnectEventCallback, type OperationCallback, type CharacteristicUpdateCallback, type Subscription, type AsyncSubscription, type BleNitroManagerOptions, BLEState, AndroidScanMode, BleNitroManager, BleTimeoutError, } from "./manager";
2
2
  export { BleNitro } from './singleton';
package/lib/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { BLEState, AndroidScanMode, BleNitroManager, } from "./manager";
1
+ export { BLEState, AndroidScanMode, BleNitroManager, BleTimeoutError, } from "./manager";
2
2
  export { BleNitro } from './singleton';
package/lib/manager.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import { BLEDevice as NativeBLEDevice, BLEState as NativeBLEState, AndroidScanMode as NativeAndroidScanMode } from './specs/NativeBleNitro';
2
+ export declare class BleTimeoutError extends Error {
3
+ constructor(operation: string, ms: number);
4
+ }
2
5
  export type ByteArray = number[];
3
6
  export interface ScanFilter {
4
7
  serviceUUIDs?: string[];
@@ -173,7 +176,10 @@ export declare class BleNitroManager {
173
176
  */
174
177
  getCharacteristics(deviceId: string, serviceId: string): string[];
175
178
  /**
176
- * Get services and characteristics for a connected device
179
+ * Get services and characteristics for a connected device.
180
+ * Uses a native method that waits for both service and characteristic
181
+ * discovery to complete before reading, avoiding the CoreBluetooth race
182
+ * where didDiscoverServices may not re-fire for cached services.
177
183
  * @param deviceId ID of the device
178
184
  * @returns Promise resolving to array of service and characteristic UUIDs
179
185
  * @see getServices
@@ -183,6 +189,8 @@ export declare class BleNitroManager {
183
189
  uuid: string;
184
190
  characteristics: string[];
185
191
  }[]>;
192
+ private static readonly DISCOVERY_TIMEOUT_MS;
193
+ private _discoverServicesWithCharacteristics;
186
194
  /**
187
195
  * Read a characteristic value
188
196
  * @param deviceId ID of the device
@@ -218,6 +226,14 @@ export declare class BleNitroManager {
218
226
  * @returns Promise resolving when unsubscription is complete
219
227
  */
220
228
  unsubscribeFromCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<void>;
229
+ /**
230
+ * Check if currently subscribed to a characteristic's notifications
231
+ * @param deviceId ID of the device
232
+ * @param serviceId ID of the service
233
+ * @param characteristicId ID of the characteristic
234
+ * @returns Boolean indicating if subscribed to notifications
235
+ */
236
+ isSubscribedToCharacteristic(deviceId: string, serviceId: string, characteristicId: string): boolean;
221
237
  /**
222
238
  * Check if Bluetooth is enabled
223
239
  * @returns returns Boolean according to Bluetooth state
package/lib/manager.js CHANGED
@@ -1,5 +1,18 @@
1
1
  import BleNitroNativeFactory from './specs/NativeBleNitroFactory';
2
2
  import { AndroidScanMode as NativeAndroidScanMode, } from './specs/NativeBleNitro';
3
+ export class BleTimeoutError extends Error {
4
+ constructor(operation, ms) {
5
+ super(`${operation} timed out after ${ms}ms`);
6
+ this.name = 'BleTimeoutError';
7
+ }
8
+ }
9
+ function withTimeout(promise, ms, operation) {
10
+ let timer;
11
+ const timeout = new Promise((_, reject) => {
12
+ timer = setTimeout(() => reject(new BleTimeoutError(operation, ms)), ms);
13
+ });
14
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
15
+ }
3
16
  export var BLEState;
4
17
  (function (BLEState) {
5
18
  BLEState["Unknown"] = "Unknown";
@@ -357,21 +370,39 @@ export class BleNitroManager {
357
370
  return BleNitroManager.normalizeGattUUIDs(characteristics);
358
371
  }
359
372
  /**
360
- * Get services and characteristics for a connected device
373
+ * Get services and characteristics for a connected device.
374
+ * Uses a native method that waits for both service and characteristic
375
+ * discovery to complete before reading, avoiding the CoreBluetooth race
376
+ * where didDiscoverServices may not re-fire for cached services.
361
377
  * @param deviceId ID of the device
362
378
  * @returns Promise resolving to array of service and characteristic UUIDs
363
379
  * @see getServices
364
380
  * @see getCharacteristics
365
381
  */
366
382
  async getServicesWithCharacteristics(deviceId) {
367
- await this.discoverServices(deviceId);
368
- const services = await this.getServices(deviceId);
369
- return services.map((service) => {
370
- return {
371
- uuid: service,
372
- characteristics: this.getCharacteristics(deviceId, service),
373
- };
383
+ await this._discoverServicesWithCharacteristics(deviceId);
384
+ const services = this.Instance.getServices(deviceId);
385
+ return BleNitroManager.normalizeGattUUIDs(services).map((service) => ({
386
+ uuid: service,
387
+ characteristics: this.getCharacteristics(deviceId, service),
388
+ }));
389
+ }
390
+ _discoverServicesWithCharacteristics(deviceId) {
391
+ const inner = new Promise((resolve, reject) => {
392
+ if (!this.isConnected(deviceId)) {
393
+ reject(new Error('Device not connected'));
394
+ return;
395
+ }
396
+ this.Instance.discoverServicesWithCharacteristics(deviceId, (success, error) => {
397
+ if (success) {
398
+ resolve();
399
+ }
400
+ else {
401
+ reject(new Error(error));
402
+ }
403
+ });
374
404
  });
405
+ return withTimeout(inner, BleNitroManager.DISCOVERY_TIMEOUT_MS, 'discoverServicesWithCharacteristics');
375
406
  }
376
407
  /**
377
408
  * Read a characteristic value
@@ -480,6 +511,18 @@ export class BleNitroManager {
480
511
  });
481
512
  });
482
513
  }
514
+ /**
515
+ * Check if currently subscribed to a characteristic's notifications
516
+ * @param deviceId ID of the device
517
+ * @param serviceId ID of the service
518
+ * @param characteristicId ID of the characteristic
519
+ * @returns Boolean indicating if subscribed to notifications
520
+ */
521
+ isSubscribedToCharacteristic(deviceId, serviceId, characteristicId) {
522
+ // No isConnected guard — both native implementations already return false
523
+ // for disconnected devices, and an extra check would introduce a TOCTOU race.
524
+ return this.Instance.isSubscribedToCharacteristic(deviceId, BleNitroManager.normalizeGattUUID(serviceId), BleNitroManager.normalizeGattUUID(characteristicId));
525
+ }
483
526
  /**
484
527
  * Check if Bluetooth is enabled
485
528
  * @returns returns Boolean according to Bluetooth state
@@ -540,3 +583,4 @@ export class BleNitroManager {
540
583
  return this.Instance.openSettings();
541
584
  }
542
585
  }
586
+ BleNitroManager.DISCOVERY_TIMEOUT_MS = 30000;
@@ -74,12 +74,15 @@ export interface NativeBleNitro extends HybridObject<{
74
74
  requestMTU(deviceId: string, mtu: number): number;
75
75
  readRSSI(deviceId: string, callback: ReadRSSICallback): void;
76
76
  discoverServices(deviceId: string, callback: OperationCallback): void;
77
+ /** Discover services and wait for all characteristic discovery to complete before resolving. */
78
+ discoverServicesWithCharacteristics(deviceId: string, callback: OperationCallback): void;
77
79
  getServices(deviceId: string): string[];
78
80
  getCharacteristics(deviceId: string, serviceId: string): string[];
79
81
  readCharacteristic(deviceId: string, serviceId: string, characteristicId: string, callback: ReadCharacteristicCallback): void;
80
82
  writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, data: BLEValue, withResponse: boolean, callback: WriteCharacteristicCallback): void;
81
83
  subscribeToCharacteristic(deviceId: string, serviceId: string, characteristicId: string, updateCallback: CharacteristicCallback, completionCallback: OperationCallback): void;
82
84
  unsubscribeFromCharacteristic(deviceId: string, serviceId: string, characteristicId: string, callback: OperationCallback): void;
85
+ isSubscribedToCharacteristic(deviceId: string, serviceId: string, characteristicId: string): boolean;
83
86
  requestBluetoothEnable(callback: OperationCallback): void;
84
87
  state(): BLEState;
85
88
  subscribeToStateChange(stateCallback: StateCallback): OperationResult;
@@ -173,6 +173,10 @@ namespace margelo::nitro::co::zyke::ble {
173
173
  static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<jni::JString> /* deviceId */, jni::alias_ref<JFunc_void_bool_std__string::javaobject> /* callback */)>("discoverServices_cxx");
174
174
  method(_javaPart, jni::make_jstring(deviceId), JFunc_void_bool_std__string_cxx::fromCpp(callback));
175
175
  }
176
+ void JHybridNativeBleNitroSpec::discoverServicesWithCharacteristics(const std::string& deviceId, const std::function<void(bool /* success */, const std::string& /* error */)>& callback) {
177
+ static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<jni::JString> /* deviceId */, jni::alias_ref<JFunc_void_bool_std__string::javaobject> /* callback */)>("discoverServicesWithCharacteristics_cxx");
178
+ method(_javaPart, jni::make_jstring(deviceId), JFunc_void_bool_std__string_cxx::fromCpp(callback));
179
+ }
176
180
  std::vector<std::string> JHybridNativeBleNitroSpec::getServices(const std::string& deviceId) {
177
181
  static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<jni::JArrayClass<jni::JString>>(jni::alias_ref<jni::JString> /* deviceId */)>("getServices");
178
182
  auto __result = method(_javaPart, jni::make_jstring(deviceId));
@@ -217,6 +221,11 @@ namespace margelo::nitro::co::zyke::ble {
217
221
  static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<jni::JString> /* deviceId */, jni::alias_ref<jni::JString> /* serviceId */, jni::alias_ref<jni::JString> /* characteristicId */, jni::alias_ref<JFunc_void_bool_std__string::javaobject> /* callback */)>("unsubscribeFromCharacteristic_cxx");
218
222
  method(_javaPart, jni::make_jstring(deviceId), jni::make_jstring(serviceId), jni::make_jstring(characteristicId), JFunc_void_bool_std__string_cxx::fromCpp(callback));
219
223
  }
224
+ bool JHybridNativeBleNitroSpec::isSubscribedToCharacteristic(const std::string& deviceId, const std::string& serviceId, const std::string& characteristicId) {
225
+ static const auto method = _javaPart->javaClassStatic()->getMethod<jboolean(jni::alias_ref<jni::JString> /* deviceId */, jni::alias_ref<jni::JString> /* serviceId */, jni::alias_ref<jni::JString> /* characteristicId */)>("isSubscribedToCharacteristic");
226
+ auto __result = method(_javaPart, jni::make_jstring(deviceId), jni::make_jstring(serviceId), jni::make_jstring(characteristicId));
227
+ return static_cast<bool>(__result);
228
+ }
220
229
  void JHybridNativeBleNitroSpec::requestBluetoothEnable(const std::function<void(bool /* success */, const std::string& /* error */)>& callback) {
221
230
  static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JFunc_void_bool_std__string::javaobject> /* callback */)>("requestBluetoothEnable_cxx");
222
231
  method(_javaPart, JFunc_void_bool_std__string_cxx::fromCpp(callback));
@@ -67,12 +67,14 @@ namespace margelo::nitro::co::zyke::ble {
67
67
  double requestMTU(const std::string& deviceId, double mtu) override;
68
68
  void readRSSI(const std::string& deviceId, const std::function<void(bool /* success */, double /* rssi */, const std::string& /* error */)>& callback) override;
69
69
  void discoverServices(const std::string& deviceId, const std::function<void(bool /* success */, const std::string& /* error */)>& callback) override;
70
+ void discoverServicesWithCharacteristics(const std::string& deviceId, const std::function<void(bool /* success */, const std::string& /* error */)>& callback) override;
70
71
  std::vector<std::string> getServices(const std::string& deviceId) override;
71
72
  std::vector<std::string> getCharacteristics(const std::string& deviceId, const std::string& serviceId) override;
72
73
  void readCharacteristic(const std::string& deviceId, const std::string& serviceId, const std::string& characteristicId, const std::function<void(bool /* success */, const std::shared_ptr<ArrayBuffer>& /* data */, const std::string& /* error */)>& callback) override;
73
74
  void writeCharacteristic(const std::string& deviceId, const std::string& serviceId, const std::string& characteristicId, const std::shared_ptr<ArrayBuffer>& data, bool withResponse, const std::function<void(bool /* success */, const std::shared_ptr<ArrayBuffer>& /* responseData */, const std::string& /* error */)>& callback) override;
74
75
  void subscribeToCharacteristic(const std::string& deviceId, const std::string& serviceId, const std::string& characteristicId, const std::function<void(const std::string& /* characteristicId */, const std::shared_ptr<ArrayBuffer>& /* data */)>& updateCallback, const std::function<void(bool /* success */, const std::string& /* error */)>& completionCallback) override;
75
76
  void unsubscribeFromCharacteristic(const std::string& deviceId, const std::string& serviceId, const std::string& characteristicId, const std::function<void(bool /* success */, const std::string& /* error */)>& callback) override;
77
+ bool isSubscribedToCharacteristic(const std::string& deviceId, const std::string& serviceId, const std::string& characteristicId) override;
76
78
  void requestBluetoothEnable(const std::function<void(bool /* success */, const std::string& /* error */)>& callback) override;
77
79
  BLEState state() override;
78
80
  OperationResult subscribeToStateChange(const std::function<void(BLEState /* state */)>& stateCallback) override;
@@ -113,6 +113,15 @@ abstract class HybridNativeBleNitroSpec: HybridObject() {
113
113
  return __result
114
114
  }
115
115
 
116
+ abstract fun discoverServicesWithCharacteristics(deviceId: String, callback: (success: Boolean, error: String) -> Unit): Unit
117
+
118
+ @DoNotStrip
119
+ @Keep
120
+ private fun discoverServicesWithCharacteristics_cxx(deviceId: String, callback: Func_void_bool_std__string): Unit {
121
+ val __result = discoverServicesWithCharacteristics(deviceId, callback)
122
+ return __result
123
+ }
124
+
116
125
  @DoNotStrip
117
126
  @Keep
118
127
  abstract fun getServices(deviceId: String): Array<String>
@@ -157,6 +166,10 @@ abstract class HybridNativeBleNitroSpec: HybridObject() {
157
166
  return __result
158
167
  }
159
168
 
169
+ @DoNotStrip
170
+ @Keep
171
+ abstract fun isSubscribedToCharacteristic(deviceId: String, serviceId: String, characteristicId: String): Boolean
172
+
160
173
  abstract fun requestBluetoothEnable(callback: (success: Boolean, error: String) -> Unit): Unit
161
174
 
162
175
  @DoNotStrip
@@ -184,6 +184,12 @@ namespace margelo::nitro::co::zyke::ble {
184
184
  std::rethrow_exception(__result.error());
185
185
  }
186
186
  }
187
+ inline void discoverServicesWithCharacteristics(const std::string& deviceId, const std::function<void(bool /* success */, const std::string& /* error */)>& callback) override {
188
+ auto __result = _swiftPart.discoverServicesWithCharacteristics(deviceId, callback);
189
+ if (__result.hasError()) [[unlikely]] {
190
+ std::rethrow_exception(__result.error());
191
+ }
192
+ }
187
193
  inline std::vector<std::string> getServices(const std::string& deviceId) override {
188
194
  auto __result = _swiftPart.getServices(deviceId);
189
195
  if (__result.hasError()) [[unlikely]] {
@@ -224,6 +230,14 @@ namespace margelo::nitro::co::zyke::ble {
224
230
  std::rethrow_exception(__result.error());
225
231
  }
226
232
  }
233
+ inline bool isSubscribedToCharacteristic(const std::string& deviceId, const std::string& serviceId, const std::string& characteristicId) override {
234
+ auto __result = _swiftPart.isSubscribedToCharacteristic(deviceId, serviceId, characteristicId);
235
+ if (__result.hasError()) [[unlikely]] {
236
+ std::rethrow_exception(__result.error());
237
+ }
238
+ auto __value = std::move(__result.value());
239
+ return __value;
240
+ }
227
241
  inline void requestBluetoothEnable(const std::function<void(bool /* success */, const std::string& /* error */)>& callback) override {
228
242
  auto __result = _swiftPart.requestBluetoothEnable(callback);
229
243
  if (__result.hasError()) [[unlikely]] {
@@ -25,12 +25,14 @@ public protocol HybridNativeBleNitroSpec_protocol: HybridObject {
25
25
  func requestMTU(deviceId: String, mtu: Double) throws -> Double
26
26
  func readRSSI(deviceId: String, callback: @escaping (_ success: Bool, _ rssi: Double, _ error: String) -> Void) throws -> Void
27
27
  func discoverServices(deviceId: String, callback: @escaping (_ success: Bool, _ error: String) -> Void) throws -> Void
28
+ func discoverServicesWithCharacteristics(deviceId: String, callback: @escaping (_ success: Bool, _ error: String) -> Void) throws -> Void
28
29
  func getServices(deviceId: String) throws -> [String]
29
30
  func getCharacteristics(deviceId: String, serviceId: String) throws -> [String]
30
31
  func readCharacteristic(deviceId: String, serviceId: String, characteristicId: String, callback: @escaping (_ success: Bool, _ data: ArrayBuffer, _ error: String) -> Void) throws -> Void
31
32
  func writeCharacteristic(deviceId: String, serviceId: String, characteristicId: String, data: ArrayBuffer, withResponse: Bool, callback: @escaping (_ success: Bool, _ responseData: ArrayBuffer, _ error: String) -> Void) throws -> Void
32
33
  func subscribeToCharacteristic(deviceId: String, serviceId: String, characteristicId: String, updateCallback: @escaping (_ characteristicId: String, _ data: ArrayBuffer) -> Void, completionCallback: @escaping (_ success: Bool, _ error: String) -> Void) throws -> Void
33
34
  func unsubscribeFromCharacteristic(deviceId: String, serviceId: String, characteristicId: String, callback: @escaping (_ success: Bool, _ error: String) -> Void) throws -> Void
35
+ func isSubscribedToCharacteristic(deviceId: String, serviceId: String, characteristicId: String) throws -> Bool
34
36
  func requestBluetoothEnable(callback: @escaping (_ success: Bool, _ error: String) -> Void) throws -> Void
35
37
  func state() throws -> BLEState
36
38
  func subscribeToStateChange(stateCallback: @escaping (_ state: BLEState) -> Void) throws -> OperationResult
@@ -370,6 +370,22 @@ open class HybridNativeBleNitroSpec_cxx {
370
370
  }
371
371
  }
372
372
 
373
+ @inline(__always)
374
+ public final func discoverServicesWithCharacteristics(deviceId: std.string, callback: bridge.Func_void_bool_std__string) -> bridge.Result_void_ {
375
+ do {
376
+ try self.__implementation.discoverServicesWithCharacteristics(deviceId: String(deviceId), callback: { () -> (Bool, String) -> Void in
377
+ let __wrappedFunction = bridge.wrap_Func_void_bool_std__string(callback)
378
+ return { (__success: Bool, __error: String) -> Void in
379
+ __wrappedFunction.call(__success, std.string(__error))
380
+ }
381
+ }())
382
+ return bridge.create_Result_void_()
383
+ } catch (let __error) {
384
+ let __exceptionPtr = __error.toCpp()
385
+ return bridge.create_Result_void_(__exceptionPtr)
386
+ }
387
+ }
388
+
373
389
  @inline(__always)
374
390
  public final func getServices(deviceId: std.string) -> bridge.Result_std__vector_std__string__ {
375
391
  do {
@@ -475,6 +491,18 @@ open class HybridNativeBleNitroSpec_cxx {
475
491
  }
476
492
  }
477
493
 
494
+ @inline(__always)
495
+ public final func isSubscribedToCharacteristic(deviceId: std.string, serviceId: std.string, characteristicId: std.string) -> bridge.Result_bool_ {
496
+ do {
497
+ let __result = try self.__implementation.isSubscribedToCharacteristic(deviceId: String(deviceId), serviceId: String(serviceId), characteristicId: String(characteristicId))
498
+ let __resultCpp = __result
499
+ return bridge.create_Result_bool_(__resultCpp)
500
+ } catch (let __error) {
501
+ let __exceptionPtr = __error.toCpp()
502
+ return bridge.create_Result_bool_(__exceptionPtr)
503
+ }
504
+ }
505
+
478
506
  @inline(__always)
479
507
  public final func requestBluetoothEnable(callback: bridge.Func_void_bool_std__string) -> bridge.Result_void_ {
480
508
  do {
@@ -28,12 +28,14 @@ namespace margelo::nitro::co::zyke::ble {
28
28
  prototype.registerHybridMethod("requestMTU", &HybridNativeBleNitroSpec::requestMTU);
29
29
  prototype.registerHybridMethod("readRSSI", &HybridNativeBleNitroSpec::readRSSI);
30
30
  prototype.registerHybridMethod("discoverServices", &HybridNativeBleNitroSpec::discoverServices);
31
+ prototype.registerHybridMethod("discoverServicesWithCharacteristics", &HybridNativeBleNitroSpec::discoverServicesWithCharacteristics);
31
32
  prototype.registerHybridMethod("getServices", &HybridNativeBleNitroSpec::getServices);
32
33
  prototype.registerHybridMethod("getCharacteristics", &HybridNativeBleNitroSpec::getCharacteristics);
33
34
  prototype.registerHybridMethod("readCharacteristic", &HybridNativeBleNitroSpec::readCharacteristic);
34
35
  prototype.registerHybridMethod("writeCharacteristic", &HybridNativeBleNitroSpec::writeCharacteristic);
35
36
  prototype.registerHybridMethod("subscribeToCharacteristic", &HybridNativeBleNitroSpec::subscribeToCharacteristic);
36
37
  prototype.registerHybridMethod("unsubscribeFromCharacteristic", &HybridNativeBleNitroSpec::unsubscribeFromCharacteristic);
38
+ prototype.registerHybridMethod("isSubscribedToCharacteristic", &HybridNativeBleNitroSpec::isSubscribedToCharacteristic);
37
39
  prototype.registerHybridMethod("requestBluetoothEnable", &HybridNativeBleNitroSpec::requestBluetoothEnable);
38
40
  prototype.registerHybridMethod("state", &HybridNativeBleNitroSpec::state);
39
41
  prototype.registerHybridMethod("subscribeToStateChange", &HybridNativeBleNitroSpec::subscribeToStateChange);
@@ -79,12 +79,14 @@ namespace margelo::nitro::co::zyke::ble {
79
79
  virtual double requestMTU(const std::string& deviceId, double mtu) = 0;
80
80
  virtual void readRSSI(const std::string& deviceId, const std::function<void(bool /* success */, double /* rssi */, const std::string& /* error */)>& callback) = 0;
81
81
  virtual void discoverServices(const std::string& deviceId, const std::function<void(bool /* success */, const std::string& /* error */)>& callback) = 0;
82
+ virtual void discoverServicesWithCharacteristics(const std::string& deviceId, const std::function<void(bool /* success */, const std::string& /* error */)>& callback) = 0;
82
83
  virtual std::vector<std::string> getServices(const std::string& deviceId) = 0;
83
84
  virtual std::vector<std::string> getCharacteristics(const std::string& deviceId, const std::string& serviceId) = 0;
84
85
  virtual void readCharacteristic(const std::string& deviceId, const std::string& serviceId, const std::string& characteristicId, const std::function<void(bool /* success */, const std::shared_ptr<ArrayBuffer>& /* data */, const std::string& /* error */)>& callback) = 0;
85
86
  virtual void writeCharacteristic(const std::string& deviceId, const std::string& serviceId, const std::string& characteristicId, const std::shared_ptr<ArrayBuffer>& data, bool withResponse, const std::function<void(bool /* success */, const std::shared_ptr<ArrayBuffer>& /* responseData */, const std::string& /* error */)>& callback) = 0;
86
87
  virtual void subscribeToCharacteristic(const std::string& deviceId, const std::string& serviceId, const std::string& characteristicId, const std::function<void(const std::string& /* characteristicId */, const std::shared_ptr<ArrayBuffer>& /* data */)>& updateCallback, const std::function<void(bool /* success */, const std::string& /* error */)>& completionCallback) = 0;
87
88
  virtual void unsubscribeFromCharacteristic(const std::string& deviceId, const std::string& serviceId, const std::string& characteristicId, const std::function<void(bool /* success */, const std::string& /* error */)>& callback) = 0;
89
+ virtual bool isSubscribedToCharacteristic(const std::string& deviceId, const std::string& serviceId, const std::string& characteristicId) = 0;
88
90
  virtual void requestBluetoothEnable(const std::function<void(bool /* success */, const std::string& /* error */)>& callback) = 0;
89
91
  virtual BLEState state() = 0;
90
92
  virtual OperationResult subscribeToStateChange(const std::function<void(BLEState /* state */)>& stateCallback) = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-ble-nitro",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "High-performance React Native BLE library built on Nitro Modules",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -10,12 +10,14 @@ const mockNativeInstance = {
10
10
  requestMTU: jest.fn(),
11
11
  readRSSI: jest.fn(),
12
12
  discoverServices: jest.fn(),
13
+ discoverServicesWithCharacteristics: jest.fn(),
13
14
  getServices: jest.fn(),
14
15
  getCharacteristics: jest.fn(),
15
16
  readCharacteristic: jest.fn(),
16
17
  writeCharacteristic: jest.fn(),
17
18
  subscribeToCharacteristic: jest.fn(),
18
19
  unsubscribeFromCharacteristic: jest.fn(),
20
+ isSubscribedToCharacteristic: jest.fn(),
19
21
  getConnectedDevices: jest.fn(),
20
22
  requestBluetoothEnable: jest.fn(),
21
23
  state: jest.fn(),
@@ -200,6 +202,90 @@ describe('BleNitro', () => {
200
202
  expect(result).toEqual([0xAA, 0xBB, 0xCC]); // Response data as ByteArray
201
203
  });
202
204
 
205
+ test('getServicesWithCharacteristics discovers and returns services with characteristics', async () => {
206
+ // First connect
207
+ mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void, _disconnectCallback?: (deviceId: string, interrupted: boolean, error: string) => void) => {
208
+ callback(true, id, '');
209
+ });
210
+ await BleManager.connect('device-svc');
211
+
212
+ mockNative.isConnected.mockImplementation((id: string) => {
213
+ return id === 'device-svc';
214
+ });
215
+
216
+ // Mock discoverServicesWithCharacteristics
217
+ mockNative.discoverServicesWithCharacteristics.mockImplementation((_deviceId: string, callback: (success: boolean, error: string) => void) => {
218
+ callback(true, '');
219
+ });
220
+
221
+ // Mock getServices to return service UUIDs
222
+ mockNative.getServices.mockReturnValue(['0000180a-0000-1000-8000-00805f9b34fb', '0000180f-0000-1000-8000-00805f9b34fb']);
223
+
224
+ // Mock getCharacteristics per service
225
+ mockNative.getCharacteristics.mockImplementation((_deviceId: string, serviceId: string) => {
226
+ if (serviceId === '0000180a-0000-1000-8000-00805f9b34fb') {
227
+ return ['00002a29-0000-1000-8000-00805f9b34fb'];
228
+ }
229
+ return ['00002a19-0000-1000-8000-00805f9b34fb'];
230
+ });
231
+
232
+ const result = await BleManager.getServicesWithCharacteristics('device-svc');
233
+
234
+ expect(mockNative.discoverServicesWithCharacteristics).toHaveBeenCalledWith(
235
+ 'device-svc',
236
+ expect.any(Function)
237
+ );
238
+ expect(result).toHaveLength(2);
239
+ expect(result[0]).toHaveProperty('uuid');
240
+ expect(result[0]).toHaveProperty('characteristics');
241
+ expect(result[0].characteristics.length).toBeGreaterThan(0);
242
+ });
243
+
244
+ test('getServicesWithCharacteristics rejects on discovery failure', async () => {
245
+ mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void, _disconnectCallback?: (deviceId: string, interrupted: boolean, error: string) => void) => {
246
+ callback(true, id, '');
247
+ });
248
+ await BleManager.connect('device-svc-fail');
249
+
250
+ mockNative.isConnected.mockImplementation((id: string) => {
251
+ return id === 'device-svc-fail';
252
+ });
253
+
254
+ mockNative.discoverServicesWithCharacteristics.mockImplementation((_deviceId: string, callback: (success: boolean, error: string) => void) => {
255
+ callback(false, 'Discovery failed');
256
+ });
257
+
258
+ await expect(BleManager.getServicesWithCharacteristics('device-svc-fail')).rejects.toThrow('Discovery failed');
259
+ });
260
+
261
+ test('getServicesWithCharacteristics rejects when not connected', async () => {
262
+ mockNative.isConnected.mockReturnValue(false);
263
+
264
+ await expect(BleManager.getServicesWithCharacteristics('unknown-device')).rejects.toThrow('Device not connected');
265
+ });
266
+
267
+ test('getServicesWithCharacteristics rejects with BleTimeoutError on timeout', async () => {
268
+ jest.useFakeTimers();
269
+
270
+ mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void) => {
271
+ callback(true, id, '');
272
+ });
273
+ await BleManager.connect('device-timeout');
274
+
275
+ mockNative.isConnected.mockImplementation((id: string) => id === 'device-timeout');
276
+
277
+ // Mock that NEVER calls its callback — simulates hung native discovery
278
+ mockNative.discoverServicesWithCharacteristics.mockImplementation(() => {});
279
+
280
+ const promise = BleManager.getServicesWithCharacteristics('device-timeout');
281
+
282
+ jest.advanceTimersByTime(30_000);
283
+
284
+ await expect(promise).rejects.toThrow('timed out after 30000ms');
285
+
286
+ jest.useRealTimers();
287
+ });
288
+
203
289
  test('readCharacteristic works after connection', async () => {
204
290
  // First connect
205
291
  mockNative.connect.mockImplementation((id: string, callback: (success: boolean, deviceId: string, error: string) => void, _disconnectCallback?: (deviceId: string, interrupted: boolean, error: string) => void) => {
@@ -304,6 +390,47 @@ describe('BleNitro', () => {
304
390
  expect(onDisconnect).toHaveBeenCalledWith(deviceId, true, 'Connection lost');
305
391
  });
306
392
 
393
+ test('isSubscribedToCharacteristic delegates to native with normalized UUIDs', () => {
394
+ mockNative.isSubscribedToCharacteristic.mockReturnValueOnce(true);
395
+
396
+ const result = BleManager.isSubscribedToCharacteristic('device', 'service', 'char');
397
+
398
+ expect(result).toBe(true);
399
+ expect(mockNative.isSubscribedToCharacteristic).toHaveBeenCalledWith(
400
+ 'device',
401
+ '0service-0000-1000-8000-00805f9b34fb',
402
+ '0000char-0000-1000-8000-00805f9b34fb'
403
+ );
404
+ });
405
+
406
+ test('isSubscribedToCharacteristic returns false when not subscribed', () => {
407
+ mockNative.isSubscribedToCharacteristic.mockReturnValueOnce(false);
408
+
409
+ const result = BleManager.isSubscribedToCharacteristic('device', 'service', 'char');
410
+
411
+ expect(result).toBe(false);
412
+ });
413
+
414
+ test('isSubscribedToCharacteristic delegates to native for unknown devices', () => {
415
+ // Native layer handles unknown/disconnected devices by returning false
416
+ mockNative.isSubscribedToCharacteristic.mockReturnValueOnce(false);
417
+
418
+ const result = BleManager.isSubscribedToCharacteristic('unknown-device', 'service', 'char');
419
+
420
+ expect(result).toBe(false);
421
+ expect(mockNative.isSubscribedToCharacteristic).toHaveBeenCalled();
422
+ });
423
+
424
+ test('isSubscribedToCharacteristic propagates native exception', () => {
425
+ mockNative.isSubscribedToCharacteristic.mockImplementationOnce(() => {
426
+ throw new Error('Native error');
427
+ });
428
+
429
+ expect(() =>
430
+ BleManager.isSubscribedToCharacteristic('device', 'service', 'char')
431
+ ).toThrow('Native error');
432
+ });
433
+
307
434
  test('readRSSI requires connected device', async () => {
308
435
  await expect(
309
436
  BleManager.readRSSI('device-not-connected')
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export {
15
15
  BLEState,
16
16
  AndroidScanMode,
17
17
  BleNitroManager,
18
+ BleTimeoutError,
18
19
  } from "./manager";
19
20
 
20
21
  export { BleNitro } from './singleton';