react-native-ble-nitro 1.2.0 → 1.3.1

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 (54) hide show
  1. package/README.md +89 -28
  2. package/android/CMakeLists.txt +32 -0
  3. package/android/build.gradle +140 -0
  4. package/android/fix-prefab.gradle +51 -0
  5. package/android/gradle.properties +5 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  8. package/android/src/main/java/com/margelo/nitro/co/zyke/ble/BleNitroBleManager.kt +899 -0
  9. package/android/src/main/java/com/margelo/nitro/co/zyke/ble/BleNitroPackage.kt +38 -0
  10. package/ios/BleNitroBleManager.swift +7 -3
  11. package/lib/commonjs/index.d.ts +15 -3
  12. package/lib/commonjs/index.d.ts.map +1 -1
  13. package/lib/commonjs/index.js +46 -28
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/commonjs/specs/NativeBleNitro.nitro.d.ts +8 -1
  16. package/lib/commonjs/specs/NativeBleNitro.nitro.d.ts.map +1 -1
  17. package/lib/commonjs/specs/NativeBleNitro.nitro.js +8 -1
  18. package/lib/commonjs/specs/NativeBleNitro.nitro.js.map +1 -1
  19. package/lib/index.d.ts +15 -3
  20. package/lib/index.js +44 -26
  21. package/lib/specs/NativeBleNitro.nitro.d.ts +8 -1
  22. package/lib/specs/NativeBleNitro.nitro.js +7 -0
  23. package/nitrogen/generated/android/BleNitroOnLoad.cpp +2 -2
  24. package/nitrogen/generated/android/c++/JAndroidScanMode.hpp +65 -0
  25. package/nitrogen/generated/android/c++/JFunc_void_std__optional_BLEDevice__std__optional_std__string_.hpp +86 -0
  26. package/nitrogen/generated/android/c++/JHybridNativeBleNitroSpec.cpp +8 -4
  27. package/nitrogen/generated/android/c++/JHybridNativeBleNitroSpec.hpp +1 -1
  28. package/nitrogen/generated/android/c++/JScanFilter.hpp +8 -2
  29. package/nitrogen/generated/android/kotlin/com/margelo/nitro/co/zyke/ble/AndroidScanMode.kt +23 -0
  30. package/nitrogen/generated/android/kotlin/com/margelo/nitro/co/zyke/ble/{Func_void_BLEDevice.kt → Func_void_std__optional_BLEDevice__std__optional_std__string_.kt} +14 -14
  31. package/nitrogen/generated/android/kotlin/com/margelo/nitro/co/zyke/ble/HybridNativeBleNitroSpec.kt +2 -2
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/co/zyke/ble/ScanFilter.kt +4 -1
  33. package/nitrogen/generated/ios/BleNitro-Swift-Cxx-Bridge.cpp +5 -5
  34. package/nitrogen/generated/ios/BleNitro-Swift-Cxx-Bridge.hpp +30 -21
  35. package/nitrogen/generated/ios/BleNitro-Swift-Cxx-Umbrella.hpp +3 -0
  36. package/nitrogen/generated/ios/c++/HybridNativeBleNitroSpecSwift.hpp +5 -2
  37. package/nitrogen/generated/ios/swift/AndroidScanMode.swift +48 -0
  38. package/nitrogen/generated/ios/swift/Func_void_std__optional_BLEDevice__std__optional_std__string_.swift +59 -0
  39. package/nitrogen/generated/ios/swift/HybridNativeBleNitroSpec.swift +1 -1
  40. package/nitrogen/generated/ios/swift/HybridNativeBleNitroSpec_cxx.swift +17 -5
  41. package/nitrogen/generated/ios/swift/ScanFilter.swift +13 -2
  42. package/nitrogen/generated/shared/c++/AndroidScanMode.hpp +64 -0
  43. package/nitrogen/generated/shared/c++/HybridNativeBleNitroSpec.hpp +3 -3
  44. package/nitrogen/generated/shared/c++/ScanFilter.hpp +9 -3
  45. package/package.json +1 -1
  46. package/plugin/build/index.d.ts +2 -0
  47. package/plugin/build/index.js +2 -0
  48. package/plugin/build/withBleNitro.d.ts +5 -1
  49. package/plugin/build/withBleNitro.js +18 -7
  50. package/src/__tests__/index.test.ts +45 -11
  51. package/src/index.ts +55 -27
  52. package/src/specs/NativeBleNitro.nitro.ts +9 -1
  53. package/nitrogen/generated/android/c++/JFunc_void_BLEDevice.hpp +0 -85
  54. package/nitrogen/generated/ios/swift/Func_void_BLEDevice.swift +0 -47
@@ -110,12 +110,24 @@ open class HybridNativeBleNitroSpec_cxx {
110
110
 
111
111
  // Methods
112
112
  @inline(__always)
113
- public final func startScan(filter: ScanFilter, callback: bridge.Func_void_BLEDevice) -> bridge.Result_void_ {
113
+ public final func startScan(filter: ScanFilter, callback: bridge.Func_void_std__optional_BLEDevice__std__optional_std__string_) -> bridge.Result_void_ {
114
114
  do {
115
- try self.__implementation.startScan(filter: filter, callback: { () -> (BLEDevice) -> Void in
116
- let __wrappedFunction = bridge.wrap_Func_void_BLEDevice(callback)
117
- return { (__device: BLEDevice) -> Void in
118
- __wrappedFunction.call(__device)
115
+ try self.__implementation.startScan(filter: filter, callback: { () -> (BLEDevice?, String?) -> Void in
116
+ let __wrappedFunction = bridge.wrap_Func_void_std__optional_BLEDevice__std__optional_std__string_(callback)
117
+ return { (__device: BLEDevice?, __error: String?) -> Void in
118
+ __wrappedFunction.call({ () -> bridge.std__optional_BLEDevice_ in
119
+ if let __unwrappedValue = __device {
120
+ return bridge.create_std__optional_BLEDevice_(__unwrappedValue)
121
+ } else {
122
+ return .init()
123
+ }
124
+ }(), { () -> bridge.std__optional_std__string_ in
125
+ if let __unwrappedValue = __error {
126
+ return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
127
+ } else {
128
+ return .init()
129
+ }
130
+ }())
119
131
  }
120
132
  }())
121
133
  return bridge.create_Result_void_()
@@ -18,14 +18,14 @@ public extension ScanFilter {
18
18
  /**
19
19
  * Create a new instance of `ScanFilter`.
20
20
  */
21
- init(serviceUUIDs: [String], rssiThreshold: Double, allowDuplicates: Bool) {
21
+ init(serviceUUIDs: [String], rssiThreshold: Double, allowDuplicates: Bool, androidScanMode: AndroidScanMode) {
22
22
  self.init({ () -> bridge.std__vector_std__string_ in
23
23
  var __vector = bridge.create_std__vector_std__string_(serviceUUIDs.count)
24
24
  for __item in serviceUUIDs {
25
25
  __vector.push_back(std.string(__item))
26
26
  }
27
27
  return __vector
28
- }(), rssiThreshold, allowDuplicates)
28
+ }(), rssiThreshold, allowDuplicates, androidScanMode)
29
29
  }
30
30
 
31
31
  var serviceUUIDs: [String] {
@@ -66,4 +66,15 @@ public extension ScanFilter {
66
66
  self.__allowDuplicates = newValue
67
67
  }
68
68
  }
69
+
70
+ var androidScanMode: AndroidScanMode {
71
+ @inline(__always)
72
+ get {
73
+ return self.__androidScanMode
74
+ }
75
+ @inline(__always)
76
+ set {
77
+ self.__androidScanMode = newValue
78
+ }
79
+ }
69
80
  }
@@ -0,0 +1,64 @@
1
+ ///
2
+ /// AndroidScanMode.hpp
3
+ /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
4
+ /// https://github.com/mrousavy/nitro
5
+ /// Copyright © 2025 Marc Rousavy @ Margelo
6
+ ///
7
+
8
+ #pragma once
9
+
10
+ #if __has_include(<NitroModules/JSIConverter.hpp>)
11
+ #include <NitroModules/JSIConverter.hpp>
12
+ #else
13
+ #error NitroModules cannot be found! Are you sure you installed NitroModules properly?
14
+ #endif
15
+ #if __has_include(<NitroModules/NitroDefines.hpp>)
16
+ #include <NitroModules/NitroDefines.hpp>
17
+ #else
18
+ #error NitroModules cannot be found! Are you sure you installed NitroModules properly?
19
+ #endif
20
+
21
+ namespace margelo::nitro::co::zyke::ble {
22
+
23
+ /**
24
+ * An enum which can be represented as a JavaScript enum (AndroidScanMode).
25
+ */
26
+ enum class AndroidScanMode {
27
+ LOWLATENCY SWIFT_NAME(lowlatency) = 0,
28
+ BALANCED SWIFT_NAME(balanced) = 1,
29
+ LOWPOWER SWIFT_NAME(lowpower) = 2,
30
+ OPPORTUNISTIC SWIFT_NAME(opportunistic) = 3,
31
+ } CLOSED_ENUM;
32
+
33
+ } // namespace margelo::nitro::co::zyke::ble
34
+
35
+ namespace margelo::nitro {
36
+
37
+ // C++ AndroidScanMode <> JS AndroidScanMode (enum)
38
+ template <>
39
+ struct JSIConverter<margelo::nitro::co::zyke::ble::AndroidScanMode> final {
40
+ static inline margelo::nitro::co::zyke::ble::AndroidScanMode fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
41
+ int enumValue = JSIConverter<int>::fromJSI(runtime, arg);
42
+ return static_cast<margelo::nitro::co::zyke::ble::AndroidScanMode>(enumValue);
43
+ }
44
+ static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::co::zyke::ble::AndroidScanMode arg) {
45
+ int enumValue = static_cast<int>(arg);
46
+ return JSIConverter<int>::toJSI(runtime, enumValue);
47
+ }
48
+ static inline bool canConvert(jsi::Runtime&, const jsi::Value& value) {
49
+ if (!value.isNumber()) {
50
+ return false;
51
+ }
52
+ double number = value.getNumber();
53
+ int integer = static_cast<int>(number);
54
+ if (number != integer) {
55
+ // The integer is not the same value as the double - we truncated floating points.
56
+ // Enums are all integers, so the input floating point number is obviously invalid.
57
+ return false;
58
+ }
59
+ // Check if we are within the bounds of the enum.
60
+ return integer >= 0 && integer <= 3;
61
+ }
62
+ };
63
+
64
+ } // namespace margelo::nitro
@@ -26,10 +26,10 @@ namespace margelo::nitro::co::zyke::ble { struct OperationResult; }
26
26
 
27
27
  #include "ScanFilter.hpp"
28
28
  #include "BLEDevice.hpp"
29
+ #include <optional>
30
+ #include <string>
29
31
  #include <functional>
30
32
  #include <vector>
31
- #include <string>
32
- #include <optional>
33
33
  #include <NitroModules/ArrayBuffer.hpp>
34
34
  #include "BLEState.hpp"
35
35
  #include "OperationResult.hpp"
@@ -66,7 +66,7 @@ namespace margelo::nitro::co::zyke::ble {
66
66
 
67
67
  public:
68
68
  // Methods
69
- virtual void startScan(const ScanFilter& filter, const std::function<void(const BLEDevice& /* device */)>& callback) = 0;
69
+ virtual void startScan(const ScanFilter& filter, const std::function<void(const std::optional<BLEDevice>& /* device */, const std::optional<std::string>& /* error */)>& callback) = 0;
70
70
  virtual bool stopScan() = 0;
71
71
  virtual bool isScanning() = 0;
72
72
  virtual std::vector<BLEDevice> getConnectedDevices(const std::vector<std::string>& services) = 0;
@@ -18,10 +18,12 @@
18
18
  #error NitroModules cannot be found! Are you sure you installed NitroModules properly?
19
19
  #endif
20
20
 
21
-
21
+ // Forward declaration of `AndroidScanMode` to properly resolve imports.
22
+ namespace margelo::nitro::co::zyke::ble { enum class AndroidScanMode; }
22
23
 
23
24
  #include <string>
24
25
  #include <vector>
26
+ #include "AndroidScanMode.hpp"
25
27
 
26
28
  namespace margelo::nitro::co::zyke::ble {
27
29
 
@@ -33,10 +35,11 @@ namespace margelo::nitro::co::zyke::ble {
33
35
  std::vector<std::string> serviceUUIDs SWIFT_PRIVATE;
34
36
  double rssiThreshold SWIFT_PRIVATE;
35
37
  bool allowDuplicates SWIFT_PRIVATE;
38
+ AndroidScanMode androidScanMode SWIFT_PRIVATE;
36
39
 
37
40
  public:
38
41
  ScanFilter() = default;
39
- explicit ScanFilter(std::vector<std::string> serviceUUIDs, double rssiThreshold, bool allowDuplicates): serviceUUIDs(serviceUUIDs), rssiThreshold(rssiThreshold), allowDuplicates(allowDuplicates) {}
42
+ explicit ScanFilter(std::vector<std::string> serviceUUIDs, double rssiThreshold, bool allowDuplicates, AndroidScanMode androidScanMode): serviceUUIDs(serviceUUIDs), rssiThreshold(rssiThreshold), allowDuplicates(allowDuplicates), androidScanMode(androidScanMode) {}
40
43
  };
41
44
 
42
45
  } // namespace margelo::nitro::co::zyke::ble
@@ -51,7 +54,8 @@ namespace margelo::nitro {
51
54
  return margelo::nitro::co::zyke::ble::ScanFilter(
52
55
  JSIConverter<std::vector<std::string>>::fromJSI(runtime, obj.getProperty(runtime, "serviceUUIDs")),
53
56
  JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, "rssiThreshold")),
54
- JSIConverter<bool>::fromJSI(runtime, obj.getProperty(runtime, "allowDuplicates"))
57
+ JSIConverter<bool>::fromJSI(runtime, obj.getProperty(runtime, "allowDuplicates")),
58
+ JSIConverter<margelo::nitro::co::zyke::ble::AndroidScanMode>::fromJSI(runtime, obj.getProperty(runtime, "androidScanMode"))
55
59
  );
56
60
  }
57
61
  static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::co::zyke::ble::ScanFilter& arg) {
@@ -59,6 +63,7 @@ namespace margelo::nitro {
59
63
  obj.setProperty(runtime, "serviceUUIDs", JSIConverter<std::vector<std::string>>::toJSI(runtime, arg.serviceUUIDs));
60
64
  obj.setProperty(runtime, "rssiThreshold", JSIConverter<double>::toJSI(runtime, arg.rssiThreshold));
61
65
  obj.setProperty(runtime, "allowDuplicates", JSIConverter<bool>::toJSI(runtime, arg.allowDuplicates));
66
+ obj.setProperty(runtime, "androidScanMode", JSIConverter<margelo::nitro::co::zyke::ble::AndroidScanMode>::toJSI(runtime, arg.androidScanMode));
62
67
  return obj;
63
68
  }
64
69
  static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
@@ -69,6 +74,7 @@ namespace margelo::nitro {
69
74
  if (!JSIConverter<std::vector<std::string>>::canConvert(runtime, obj.getProperty(runtime, "serviceUUIDs"))) return false;
70
75
  if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, "rssiThreshold"))) return false;
71
76
  if (!JSIConverter<bool>::canConvert(runtime, obj.getProperty(runtime, "allowDuplicates"))) return false;
77
+ if (!JSIConverter<margelo::nitro::co::zyke::ble::AndroidScanMode>::canConvert(runtime, obj.getProperty(runtime, "androidScanMode"))) return false;
72
78
  return true;
73
79
  }
74
80
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-ble-nitro",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
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",
@@ -15,6 +15,8 @@ import withBleNitro, { withBleNitroAndroid, withBleNitroIOS, type BleNitroPlugin
15
15
  * {
16
16
  * "isBackgroundEnabled": true,
17
17
  * "modes": ["peripheral", "central"],
18
+ * "neverForLocation": true,
19
+ * "androidAdvertisingEnabled": true,
18
20
  * "bluetoothAlwaysPermission": "Allow $(PRODUCT_NAME) to connect to bluetooth devices"
19
21
  * }
20
22
  * ]
@@ -53,6 +53,8 @@ Object.defineProperty(exports, "withBleNitroIOS", { enumerable: true, get: funct
53
53
  * {
54
54
  * "isBackgroundEnabled": true,
55
55
  * "modes": ["peripheral", "central"],
56
+ * "neverForLocation": true,
57
+ * "androidAdvertisingEnabled": true,
56
58
  * "bluetoothAlwaysPermission": "Allow $(PRODUCT_NAME) to connect to bluetooth devices"
57
59
  * }
58
60
  * ]
@@ -11,7 +11,6 @@ export interface BleNitroPluginProps {
11
11
  * physical location from Bluetooth scan results. The location permission
12
12
  * will still be required on older Android devices.
13
13
  * @default false
14
- * @warning This parameter is experimental and BLE might not work. Test before releasing.
15
14
  */
16
15
  neverForLocation?: boolean;
17
16
  /**
@@ -24,6 +23,11 @@ export interface BleNitroPluginProps {
24
23
  * @default "Allow $(PRODUCT_NAME) to connect to bluetooth devices"
25
24
  */
26
25
  bluetoothAlwaysPermission?: string | false;
26
+ /**
27
+ * Android advertising permission message. Set to false to skip adding the permission.
28
+ * @default "Allow $(PRODUCT_NAME) to advertise bluetooth devices"
29
+ */
30
+ androidAdvertisingEnabled?: boolean;
27
31
  }
28
32
  export declare const withBleNitroAndroid: ConfigPlugin<BleNitroPluginProps>;
29
33
  export declare const withBleNitroIOS: ConfigPlugin<BleNitroPluginProps>;
@@ -5,22 +5,26 @@ const config_plugins_1 = require("@expo/config-plugins");
5
5
  const BLUETOOTH_PERMISSIONS = [
6
6
  'android.permission.BLUETOOTH',
7
7
  'android.permission.BLUETOOTH_ADMIN',
8
- 'android.permission.ACCESS_COARSE_LOCATION',
8
+ // 'android.permission.ACCESS_COARSE_LOCATION',
9
9
  'android.permission.ACCESS_FINE_LOCATION',
10
10
  ];
11
11
  const BLUETOOTH_PERMISSIONS_API_31 = [
12
12
  'android.permission.BLUETOOTH_SCAN',
13
- 'android.permission.BLUETOOTH_ADVERTISE',
13
+ // 'android.permission.BLUETOOTH_ADVERTISE',
14
14
  'android.permission.BLUETOOTH_CONNECT',
15
15
  ];
16
+ const BLUETOOTH_ADVERTISE_API_31 = [
17
+ 'android.permission.BLUETOOTH_ADVERTISE',
18
+ ];
16
19
  const withBleNitroAndroid = (config, props = {}) => {
17
- const { isBackgroundEnabled = false, neverForLocation = false } = props;
20
+ const { isBackgroundEnabled = false, neverForLocation = false, androidAdvertisingEnabled = false } = props;
18
21
  return (0, config_plugins_1.withAndroidManifest)(config, (config) => {
19
22
  const androidManifest = config.modResults;
20
23
  // Add required permissions
21
24
  config_plugins_1.AndroidConfig.Permissions.ensurePermissions(config.modResults, [
22
25
  ...BLUETOOTH_PERMISSIONS,
23
26
  ...BLUETOOTH_PERMISSIONS_API_31,
27
+ ...(androidAdvertisingEnabled ? BLUETOOTH_ADVERTISE_API_31 : []),
24
28
  ]);
25
29
  // Add uses-feature for BLE
26
30
  if (!androidManifest.manifest['uses-feature']) {
@@ -38,16 +42,23 @@ const withBleNitroAndroid = (config, props = {}) => {
38
42
  usesFeatures.push(bleFeature);
39
43
  }
40
44
  // Handle location permission settings for Android 12+
45
+ const permissions = androidManifest.manifest['uses-permission'] || [];
41
46
  if (neverForLocation) {
42
- // Add neverForLocation attribute to location permissions for Android 12+
43
- const permissions = androidManifest.manifest['uses-permission'] || [];
44
47
  permissions.forEach((permission) => {
45
- if (permission.$?.['android:name'] === 'android.permission.ACCESS_FINE_LOCATION' ||
46
- permission.$?.['android:name'] === 'android.permission.ACCESS_COARSE_LOCATION') {
48
+ if (permission.$?.['android:name'] === 'android.permission.ACCESS_FINE_LOCATION') {
49
+ permission.$['android:maxSdkVersion'] = '30';
50
+ }
51
+ if (permission.$?.['android:name'] === 'android.permission.BLUETOOTH_SCAN') {
47
52
  permission.$['android:usesPermissionFlags'] = 'neverForLocation';
48
53
  }
49
54
  });
50
55
  }
56
+ permissions.forEach((permission) => {
57
+ if (permission.$?.['android:name'] === 'android.permission.BLUETOOTH' ||
58
+ permission.$?.['android:name'] === 'android.permission.BLUETOOTH_ADMIN') {
59
+ permission.$['android:maxSdkVersion'] = '30';
60
+ }
61
+ });
51
62
  return config;
52
63
  });
53
64
  };
@@ -8,6 +8,7 @@ jest.mock('../specs/NativeBleNitro', () => ({
8
8
  connect: jest.fn(),
9
9
  disconnect: jest.fn(),
10
10
  isConnected: jest.fn(),
11
+ requestMTU: jest.fn(),
11
12
  discoverServices: jest.fn(),
12
13
  getServices: jest.fn(),
13
14
  getCharacteristics: jest.fn(),
@@ -15,12 +16,27 @@ jest.mock('../specs/NativeBleNitro', () => ({
15
16
  writeCharacteristic: jest.fn(),
16
17
  subscribeToCharacteristic: jest.fn(),
17
18
  unsubscribeFromCharacteristic: jest.fn(),
18
- isBluetoothEnabled: jest.fn(),
19
+ getConnectedDevices: jest.fn(),
19
20
  requestBluetoothEnable: jest.fn(),
20
21
  state: jest.fn(),
21
22
  subscribeToStateChange: jest.fn(),
23
+ unsubscribeFromStateChange: jest.fn(),
24
+ openSettings: jest.fn(),
25
+ },
26
+ BLEState: {
27
+ Unknown: 0,
28
+ Resetting: 1,
29
+ Unsupported: 2,
30
+ Unauthorized: 3,
31
+ PoweredOff: 4,
32
+ PoweredOn: 5
33
+ },
34
+ AndroidScanMode: {
35
+ LowLatency: 0,
36
+ Balanced: 1,
37
+ LowPower: 2,
38
+ Opportunistic: 3
22
39
  },
23
- BLEState: { PoweredOn: 5, PoweredOff: 4 },
24
40
  }));
25
41
 
26
42
  import { ble as BleNitro } from '../index';
@@ -46,15 +62,23 @@ describe('BleNitro', () => {
46
62
  serviceUUIDs: ['test'],
47
63
  rssiThreshold: -100,
48
64
  allowDuplicates: false,
65
+ androidScanMode: 1, // AndroidScanMode.Balanced (default)
49
66
  },
50
67
  expect.any(Function)
51
68
  );
52
69
  });
53
70
 
54
- test('stopScan calls native and resolves', () => {
55
- mockNative.stopScan.mockImplementation(() => {
56
- // stopScan is void, no callback needed
71
+ test('stopScan calls native and resolves', async () => {
72
+ // First start a scan to set _isScanning to true
73
+ mockNative.startScan.mockImplementation((filter, callback) => { // eslint-disable-line @typescript-eslint/no-unused-vars
74
+ // Just start scanning
57
75
  });
76
+
77
+ const scanCallback = jest.fn();
78
+ await BleNitro.startScan({ serviceUUIDs: ['test'] }, scanCallback);
79
+
80
+ // Now stop the scan
81
+ mockNative.stopScan.mockImplementation(() => true);
58
82
 
59
83
  BleNitro.stopScan();
60
84
 
@@ -91,8 +115,9 @@ describe('BleNitro', () => {
91
115
  });
92
116
 
93
117
  test('writeCharacteristic requires connected device', async () => {
118
+ const data = [1, 2, 3];
94
119
  await expect(
95
- BleNitro.writeCharacteristic('device', 'service', 'char', [1, 2, 3])
120
+ BleNitro.writeCharacteristic('device', 'service', 'char',data)
96
121
  ).rejects.toThrow('Device not connected');
97
122
  });
98
123
 
@@ -104,8 +129,9 @@ describe('BleNitro', () => {
104
129
  await BleNitro.connect('device');
105
130
 
106
131
  // Then read
107
- mockNative.readCharacteristic.mockImplementation((_device: string, _service: string, _char: string, callback: (success: boolean, data: number[], error: string) => void) => {
108
- callback(true, [85], ''); // Battery level 85%
132
+ mockNative.readCharacteristic.mockImplementation((_device: string, _service: string, _char: string, callback: (success: boolean, data: ArrayBuffer, error: string) => void) => {
133
+ const testData = new Uint8Array([85]);
134
+ callback(true, testData.buffer, ''); // Battery level 85%
109
135
  });
110
136
 
111
137
  const result = await BleNitro.readCharacteristic('device', 'service', 'char');
@@ -117,6 +143,9 @@ describe('BleNitro', () => {
117
143
  '0000char-0000-1000-8000-00805f9b34fb', // 'char' padded to 8 chars
118
144
  expect.any(Function)
119
145
  );
146
+
147
+ // Result should be number array (ByteArray)
148
+ expect(Array.isArray(result)).toBe(true);
120
149
  expect(result).toEqual([85]);
121
150
  });
122
151
 
@@ -146,17 +175,22 @@ describe('BleNitro', () => {
146
175
  await BleNitro.connect('device');
147
176
 
148
177
  // Mock subscription
149
- mockNative.subscribeToCharacteristic.mockImplementation((_device: string, _service: string, _char: string, updateCallback: (charId: string, data: number[]) => void, resultCallback: (success: boolean, error: string) => void) => {
178
+ mockNative.subscribeToCharacteristic.mockImplementation((_device: string, _service: string, _char: string, updateCallback: (charId: string, data: ArrayBuffer) => void, resultCallback: (success: boolean, error: string) => void) => {
150
179
  resultCallback(true, '');
151
180
  // Simulate notification
152
- updateCallback('char-id', [1, 2, 3]);
181
+ const testData = new Uint8Array([1, 2, 3]);
182
+ updateCallback('char-id', testData.buffer);
153
183
  });
154
184
 
155
185
  const notificationCallback = jest.fn();
156
- BleNitro.subscribeToCharacteristic('device', 'service', 'char', notificationCallback);
186
+ const subscription = BleNitro.subscribeToCharacteristic('device', 'service', 'char', notificationCallback);
157
187
 
158
188
  expect(mockNative.subscribeToCharacteristic).toHaveBeenCalled();
159
189
  expect(notificationCallback).toHaveBeenCalledWith('char-id', [1, 2, 3]);
190
+
191
+ // Verify subscription object
192
+ expect(subscription).toHaveProperty('remove');
193
+ expect(typeof subscription.remove).toBe('function');
160
194
  });
161
195
 
162
196
  test('connect with disconnect event callback', async () => {
package/src/index.ts CHANGED
@@ -3,6 +3,8 @@ import {
3
3
  ScanFilter as NativeScanFilter,
4
4
  BLEDevice as NativeBLEDevice,
5
5
  BLEState as NativeBLEState,
6
+ ScanCallback as NativeScanCallback,
7
+ AndroidScanMode as NativeAndroidScanMode,
6
8
  } from './specs/NativeBleNitro';
7
9
 
8
10
  export type ByteArray = number[];
@@ -11,6 +13,7 @@ export interface ScanFilter {
11
13
  serviceUUIDs?: string[];
12
14
  rssiThreshold?: number;
13
15
  allowDuplicates?: boolean;
16
+ androidScanMode?: AndroidScanMode;
14
17
  }
15
18
 
16
19
  export interface ManufacturerDataEntry {
@@ -32,6 +35,7 @@ export interface BLEDevice {
32
35
  }
33
36
 
34
37
  export type ScanCallback = (device: BLEDevice) => void;
38
+ export type RestoreStateCallback = (connectedPeripherals: BLEDevice[]) => void;
35
39
  export type ConnectionCallback = (
36
40
  success: boolean,
37
41
  deviceId: string,
@@ -61,6 +65,18 @@ export enum BLEState {
61
65
  PoweredOn = 'PoweredOn',
62
66
  };
63
67
 
68
+ export enum AndroidScanMode {
69
+ LowLatency = 'LowLatency',
70
+ Balanced = 'Balanced',
71
+ LowPower = 'LowPower',
72
+ Opportunistic = 'Opportunistic',
73
+ }
74
+
75
+ export type BleNitroOptions = {
76
+ restoreStateIdentifier?: string;
77
+ onRestoreState?: RestoreStateCallback;
78
+ };
79
+
64
80
  function mapNativeBLEStateToBLEState(nativeState: NativeBLEState): BLEState {
65
81
  const map = {
66
82
  0: BLEState.Unknown,
@@ -73,6 +89,29 @@ function mapNativeBLEStateToBLEState(nativeState: NativeBLEState): BLEState {
73
89
  return map[nativeState];
74
90
  }
75
91
 
92
+ function mapAndroidScanModeToNativeAndroidScanMode(scanMode: AndroidScanMode): NativeAndroidScanMode {
93
+ const map = {
94
+ LowLatency: NativeAndroidScanMode.LowLatency,
95
+ Balanced: NativeAndroidScanMode.Balanced,
96
+ LowPower: NativeAndroidScanMode.LowPower,
97
+ Opportunistic: NativeAndroidScanMode.Opportunistic,
98
+ }
99
+ return map[scanMode];
100
+ }
101
+
102
+ function convertNativeBleDeviceToBleDevice(nativeBleDevice: NativeBLEDevice): BLEDevice {
103
+ return {
104
+ ...nativeBleDevice,
105
+ serviceUUIDs: BleNitro.normalizeGattUUIDs(nativeBleDevice.serviceUUIDs),
106
+ manufacturerData: {
107
+ companyIdentifiers: nativeBleDevice.manufacturerData.companyIdentifiers.map(entry => ({
108
+ id: entry.id,
109
+ data: arrayBufferToByteArray(entry.data)
110
+ }))
111
+ }
112
+ }
113
+ }
114
+
76
115
  function arrayBufferToByteArray(buffer: ArrayBuffer): ByteArray {
77
116
  return Array.from(new Uint8Array(buffer));
78
117
  }
@@ -126,7 +165,8 @@ export class BleNitro {
126
165
  */
127
166
  public startScan(
128
167
  filter: ScanFilter = {},
129
- callback: ScanCallback
168
+ callback: ScanCallback,
169
+ onError?: (error: string) => void,
130
170
  ): void {
131
171
  if (this._isScanning) {
132
172
  return;
@@ -137,21 +177,19 @@ export class BleNitro {
137
177
  serviceUUIDs: filter.serviceUUIDs || [],
138
178
  rssiThreshold: filter.rssiThreshold ?? -100,
139
179
  allowDuplicates: filter.allowDuplicates ?? false,
180
+ androidScanMode: mapAndroidScanModeToNativeAndroidScanMode(filter.androidScanMode ?? AndroidScanMode.Balanced),
140
181
  };
141
182
 
142
183
  // Create callback wrapper
143
- const scanCallback = (device: NativeBLEDevice) => {
184
+ const scanCallback: NativeScanCallback = (device: NativeBLEDevice | null, error: string | null) => {
185
+ if (error && !device) {
186
+ this._isScanning = false;
187
+ onError?.(error);
188
+ return;
189
+ }
190
+ device = device!; // eslint-disable-line @typescript-eslint/no-non-null-assertion
144
191
  // Convert manufacturer data to Uint8Arrays
145
- const convertedDevice: BLEDevice = {
146
- ...device,
147
- serviceUUIDs: BleNitro.normalizeGattUUIDs(device.serviceUUIDs),
148
- manufacturerData: {
149
- companyIdentifiers: device.manufacturerData.companyIdentifiers.map(entry => ({
150
- id: entry.id,
151
- data: arrayBufferToByteArray(entry.data)
152
- }))
153
- }
154
- };
192
+ const convertedDevice: BLEDevice = convertNativeBleDeviceToBleDevice(device);
155
193
  callback(convertedDevice);
156
194
  };
157
195
 
@@ -170,6 +208,7 @@ export class BleNitro {
170
208
  }
171
209
 
172
210
  BleNitroNative.stopScan();
211
+ this._isScanning = false;
173
212
  }
174
213
 
175
214
  /**
@@ -189,16 +228,7 @@ export class BleNitro {
189
228
  public getConnectedDevices(services?: string[]): BLEDevice[] {
190
229
  const devices = BleNitroNative.getConnectedDevices(services || []);
191
230
  // Normalize service UUIDs - manufacturer data already comes as ArrayBuffers
192
- return devices.map(device => ({
193
- ...device,
194
- serviceUUIDs: BleNitro.normalizeGattUUIDs(device.serviceUUIDs),
195
- manufacturerData: {
196
- companyIdentifiers: device.manufacturerData.companyIdentifiers.map(entry => ({
197
- id: entry.id,
198
- data: arrayBufferToByteArray(entry.data)
199
- }))
200
- }
201
- }));
231
+ return devices.map(device => convertNativeBleDeviceToBleDevice(device));
202
232
  }
203
233
 
204
234
  /**
@@ -360,7 +390,7 @@ export class BleNitro {
360
390
  * @param deviceId ID of the device
361
391
  * @param serviceId ID of the service
362
392
  * @param characteristicId ID of the characteristic
363
- * @returns Promise resolving to the characteristic data as ByteArray
393
+ * @returns Promise resolving to the characteristic data as ArrayBuffer
364
394
  */
365
395
  public readCharacteristic(
366
396
  deviceId: string,
@@ -394,7 +424,7 @@ export class BleNitro {
394
424
  * @param deviceId ID of the device
395
425
  * @param serviceId ID of the service
396
426
  * @param characteristicId ID of the characteristic
397
- * @param data Data to write as ByteArray(number[])
427
+ * @param data Data to write as ByteArray (number[])
398
428
  * @param withResponse Whether to wait for response
399
429
  * @returns Promise resolving when write is complete
400
430
  */
@@ -412,13 +442,11 @@ export class BleNitro {
412
442
  return;
413
443
  }
414
444
 
415
- const dataAsArrayBuffer = byteArrayToArrayBuffer(data);
416
-
417
445
  BleNitroNative.writeCharacteristic(
418
446
  deviceId,
419
447
  BleNitro.normalizeGattUUID(serviceId),
420
448
  BleNitro.normalizeGattUUID(characteristicId),
421
- dataAsArrayBuffer,
449
+ byteArrayToArrayBuffer(data),
422
450
  withResponse,
423
451
  (success: boolean, error: string) => {
424
452
  if (success) {
@@ -31,13 +31,21 @@ export interface BLEDevice {
31
31
  isConnectable: boolean;
32
32
  }
33
33
 
34
+ export enum AndroidScanMode {
35
+ LowLatency = 0,
36
+ Balanced = 1,
37
+ LowPower = 2,
38
+ Opportunistic = 3,
39
+ }
40
+
34
41
  export interface ScanFilter {
35
42
  serviceUUIDs: string[];
36
43
  rssiThreshold: number;
37
44
  allowDuplicates: boolean;
45
+ androidScanMode: AndroidScanMode;
38
46
  }
39
47
 
40
- export type ScanCallback = (device: BLEDevice) => void;
48
+ export type ScanCallback = (device: BLEDevice | null, error: string | null) => void;
41
49
  export type DevicesCallback = (devices: BLEDevice[]) => void;
42
50
  export type ConnectionCallback = (success: boolean, deviceId: string, error: string) => void;
43
51
  export type DisconnectionEventCallback = (deviceId: string, interrupted: boolean, error: string) => void;