react-native-ble-nitro 1.2.0 → 1.3.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 (54) hide show
  1. package/README.md +81 -18
  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 +14 -8
  12. package/lib/commonjs/index.d.ts.map +1 -1
  13. package/lib/commonjs/index.js +35 -17
  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 +14 -8
  20. package/lib/index.js +33 -15
  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 +48 -13
  51. package/src/index.ts +40 -21
  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.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",
@@ -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 = new Uint8Array([1, 2, 3]);
94
119
  await expect(
95
- BleNitro.writeCharacteristic('device', 'service', 'char', [1, 2, 3])
120
+ BleNitro.writeCharacteristic('device', 'service', 'char', data.buffer)
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,7 +143,11 @@ describe('BleNitro', () => {
117
143
  '0000char-0000-1000-8000-00805f9b34fb', // 'char' padded to 8 chars
118
144
  expect.any(Function)
119
145
  );
120
- expect(result).toEqual([85]);
146
+
147
+ // Result should be ArrayBuffer
148
+ expect(result).toBeInstanceOf(ArrayBuffer);
149
+ const resultArray = new Uint8Array(result);
150
+ expect(resultArray[0]).toBe(85);
121
151
  });
122
152
 
123
153
  test('disconnect calls native', async () => {
@@ -146,17 +176,22 @@ describe('BleNitro', () => {
146
176
  await BleNitro.connect('device');
147
177
 
148
178
  // Mock subscription
149
- mockNative.subscribeToCharacteristic.mockImplementation((_device: string, _service: string, _char: string, updateCallback: (charId: string, data: number[]) => void, resultCallback: (success: boolean, error: string) => void) => {
179
+ mockNative.subscribeToCharacteristic.mockImplementation((_device: string, _service: string, _char: string, updateCallback: (charId: string, data: ArrayBuffer) => void, resultCallback: (success: boolean, error: string) => void) => {
150
180
  resultCallback(true, '');
151
181
  // Simulate notification
152
- updateCallback('char-id', [1, 2, 3]);
182
+ const testData = new Uint8Array([1, 2, 3]);
183
+ updateCallback('char-id', testData.buffer);
153
184
  });
154
185
 
155
186
  const notificationCallback = jest.fn();
156
- BleNitro.subscribeToCharacteristic('device', 'service', 'char', notificationCallback);
187
+ const subscription = BleNitro.subscribeToCharacteristic('device', 'service', 'char', notificationCallback);
157
188
 
158
189
  expect(mockNative.subscribeToCharacteristic).toHaveBeenCalled();
159
- expect(notificationCallback).toHaveBeenCalledWith('char-id', [1, 2, 3]);
190
+ expect(notificationCallback).toHaveBeenCalledWith('char-id', expect.any(ArrayBuffer));
191
+
192
+ // Verify subscription object
193
+ expect(subscription).toHaveProperty('remove');
194
+ expect(typeof subscription.remove).toBe('function');
160
195
  });
161
196
 
162
197
  test('connect with disconnect event callback', async () => {
package/src/index.ts CHANGED
@@ -3,19 +3,21 @@ 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
- export type ByteArray = number[];
9
10
 
10
11
  export interface ScanFilter {
11
12
  serviceUUIDs?: string[];
12
13
  rssiThreshold?: number;
13
14
  allowDuplicates?: boolean;
15
+ androidScanMode?: AndroidScanMode;
14
16
  }
15
17
 
16
18
  export interface ManufacturerDataEntry {
17
19
  id: string;
18
- data: ByteArray;
20
+ data: ArrayBuffer;
19
21
  }
20
22
 
21
23
  export interface ManufacturerData {
@@ -45,7 +47,7 @@ export type DisconnectEventCallback = (
45
47
  export type OperationCallback = (success: boolean, error: string) => void;
46
48
  export type CharacteristicUpdateCallback = (
47
49
  characteristicId: string,
48
- data: ByteArray
50
+ data: ArrayBuffer
49
51
  ) => void;
50
52
 
51
53
  export type Subscription = {
@@ -61,6 +63,13 @@ export enum BLEState {
61
63
  PoweredOn = 'PoweredOn',
62
64
  };
63
65
 
66
+ export enum AndroidScanMode {
67
+ LowLatency = 'LowLatency',
68
+ Balanced = 'Balanced',
69
+ LowPower = 'LowPower',
70
+ Opportunistic = 'Opportunistic',
71
+ }
72
+
64
73
  function mapNativeBLEStateToBLEState(nativeState: NativeBLEState): BLEState {
65
74
  const map = {
66
75
  0: BLEState.Unknown,
@@ -73,13 +82,16 @@ function mapNativeBLEStateToBLEState(nativeState: NativeBLEState): BLEState {
73
82
  return map[nativeState];
74
83
  }
75
84
 
76
- function arrayBufferToByteArray(buffer: ArrayBuffer): ByteArray {
77
- return Array.from(new Uint8Array(buffer));
85
+ function mapAndroidScanModeToNativeAndroidScanMode(scanMode: AndroidScanMode): NativeAndroidScanMode {
86
+ const map = {
87
+ LowLatency: NativeAndroidScanMode.LowLatency,
88
+ Balanced: NativeAndroidScanMode.Balanced,
89
+ LowPower: NativeAndroidScanMode.LowPower,
90
+ Opportunistic: NativeAndroidScanMode.Opportunistic,
91
+ }
92
+ return map[scanMode];
78
93
  }
79
94
 
80
- function byteArrayToArrayBuffer(data: ByteArray): ArrayBuffer {
81
- return new Uint8Array(data).buffer;
82
- }
83
95
 
84
96
  let _instance: BleNitro;
85
97
 
@@ -126,7 +138,8 @@ export class BleNitro {
126
138
  */
127
139
  public startScan(
128
140
  filter: ScanFilter = {},
129
- callback: ScanCallback
141
+ callback: ScanCallback,
142
+ onError?: (error: string) => void,
130
143
  ): void {
131
144
  if (this._isScanning) {
132
145
  return;
@@ -137,10 +150,17 @@ export class BleNitro {
137
150
  serviceUUIDs: filter.serviceUUIDs || [],
138
151
  rssiThreshold: filter.rssiThreshold ?? -100,
139
152
  allowDuplicates: filter.allowDuplicates ?? false,
153
+ androidScanMode: mapAndroidScanModeToNativeAndroidScanMode(filter.androidScanMode ?? AndroidScanMode.Balanced),
140
154
  };
141
155
 
142
156
  // Create callback wrapper
143
- const scanCallback = (device: NativeBLEDevice) => {
157
+ const scanCallback: NativeScanCallback = (device: NativeBLEDevice | null, error: string | null) => {
158
+ if (error && !device) {
159
+ this._isScanning = false;
160
+ onError?.(error);
161
+ return;
162
+ }
163
+ device = device!; // eslint-disable-line @typescript-eslint/no-non-null-assertion
144
164
  // Convert manufacturer data to Uint8Arrays
145
165
  const convertedDevice: BLEDevice = {
146
166
  ...device,
@@ -148,7 +168,7 @@ export class BleNitro {
148
168
  manufacturerData: {
149
169
  companyIdentifiers: device.manufacturerData.companyIdentifiers.map(entry => ({
150
170
  id: entry.id,
151
- data: arrayBufferToByteArray(entry.data)
171
+ data: entry.data
152
172
  }))
153
173
  }
154
174
  };
@@ -170,6 +190,7 @@ export class BleNitro {
170
190
  }
171
191
 
172
192
  BleNitroNative.stopScan();
193
+ this._isScanning = false;
173
194
  }
174
195
 
175
196
  /**
@@ -195,7 +216,7 @@ export class BleNitro {
195
216
  manufacturerData: {
196
217
  companyIdentifiers: device.manufacturerData.companyIdentifiers.map(entry => ({
197
218
  id: entry.id,
198
- data: arrayBufferToByteArray(entry.data)
219
+ data: entry.data
199
220
  }))
200
221
  }
201
222
  }));
@@ -360,13 +381,13 @@ export class BleNitro {
360
381
  * @param deviceId ID of the device
361
382
  * @param serviceId ID of the service
362
383
  * @param characteristicId ID of the characteristic
363
- * @returns Promise resolving to the characteristic data as ByteArray
384
+ * @returns Promise resolving to the characteristic data as ArrayBuffer
364
385
  */
365
386
  public readCharacteristic(
366
387
  deviceId: string,
367
388
  serviceId: string,
368
389
  characteristicId: string
369
- ): Promise<ByteArray> {
390
+ ): Promise<ArrayBuffer> {
370
391
  return new Promise((resolve, reject) => {
371
392
  // Check if connected first
372
393
  if (!this._connectedDevices[deviceId]) {
@@ -380,7 +401,7 @@ export class BleNitro {
380
401
  BleNitro.normalizeGattUUID(characteristicId),
381
402
  (success: boolean, data: ArrayBuffer, error: string) => {
382
403
  if (success) {
383
- resolve(arrayBufferToByteArray(data));
404
+ resolve(data);
384
405
  } else {
385
406
  reject(new Error(error));
386
407
  }
@@ -394,7 +415,7 @@ export class BleNitro {
394
415
  * @param deviceId ID of the device
395
416
  * @param serviceId ID of the service
396
417
  * @param characteristicId ID of the characteristic
397
- * @param data Data to write as ByteArray(number[])
418
+ * @param data Data to write as ArrayBuffer
398
419
  * @param withResponse Whether to wait for response
399
420
  * @returns Promise resolving when write is complete
400
421
  */
@@ -402,7 +423,7 @@ export class BleNitro {
402
423
  deviceId: string,
403
424
  serviceId: string,
404
425
  characteristicId: string,
405
- data: ByteArray,
426
+ data: ArrayBuffer,
406
427
  withResponse: boolean = true
407
428
  ): Promise<boolean> {
408
429
  return new Promise((resolve, reject) => {
@@ -412,13 +433,11 @@ export class BleNitro {
412
433
  return;
413
434
  }
414
435
 
415
- const dataAsArrayBuffer = byteArrayToArrayBuffer(data);
416
-
417
436
  BleNitroNative.writeCharacteristic(
418
437
  deviceId,
419
438
  BleNitro.normalizeGattUUID(serviceId),
420
439
  BleNitro.normalizeGattUUID(characteristicId),
421
- dataAsArrayBuffer,
440
+ data,
422
441
  withResponse,
423
442
  (success: boolean, error: string) => {
424
443
  if (success) {
@@ -457,7 +476,7 @@ export class BleNitro {
457
476
  BleNitro.normalizeGattUUID(serviceId),
458
477
  BleNitro.normalizeGattUUID(characteristicId),
459
478
  (charId: string, data: ArrayBuffer) => {
460
- callback(charId, arrayBufferToByteArray(data));
479
+ callback(charId, data);
461
480
  },
462
481
  (success, error) => {
463
482
  _success = 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;