@blunt-ly/ble-advertiser 0.0.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.
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/android/build.gradle +43 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/bleadvertiser/BleAdvertiserModule.kt +173 -0
- package/build/BleAdvertiser.types.d.ts +18 -0
- package/build/BleAdvertiser.types.d.ts.map +1 -0
- package/build/BleAdvertiser.types.js +2 -0
- package/build/BleAdvertiser.types.js.map +1 -0
- package/build/BleAdvertiserModule.d.ts +31 -0
- package/build/BleAdvertiserModule.d.ts.map +1 -0
- package/build/BleAdvertiserModule.js +4 -0
- package/build/BleAdvertiserModule.js.map +1 -0
- package/build/index.d.ts +46 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +70 -0
- package/build/index.js.map +1 -0
- package/build/utils.d.ts +9 -0
- package/build/utils.d.ts.map +1 -0
- package/build/utils.js +19 -0
- package/build/utils.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/BleAdvertiser.podspec +22 -0
- package/ios/BleAdvertiserModule.swift +246 -0
- package/package.json +53 -0
- package/src/BleAdvertiser.types.ts +20 -0
- package/src/BleAdvertiserModule.ts +33 -0
- package/src/index.ts +90 -0
- package/src/utils.ts +21 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, The Blunt.ly Authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# BLE Advertiser Module
|
|
2
|
+
|
|
3
|
+
A cross-platform BLE (Bluetooth Low Energy) advertiser module for React Native using Expo Modules API. This module allows you to advertise service UUIDs over BLE on both Android and iOS platforms, using minimal platform features to support background processing.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @blunt-ly/ble-advertiser
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
|
|
13
|
+
### Methods
|
|
14
|
+
|
|
15
|
+
#### `broadcast(uuids: string[]): Promise<string>`
|
|
16
|
+
|
|
17
|
+
Start BLE advertising with the specified service UUIDs.
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import * as BleAdvertiser from '@blunt-ly/ble-advertiser';
|
|
21
|
+
|
|
22
|
+
const serviceUUIDs = [
|
|
23
|
+
'6ba7b810-9dad-11d1-80b4-00c04fd430c8',
|
|
24
|
+
'180D' // 16-bit UUIDs are also supported
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const result = await BleAdvertiser.broadcast(serviceUUIDs);
|
|
29
|
+
console.log('Advertising started:', result);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error('Failed to start advertising:', error);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
> [!NOTE]
|
|
36
|
+
> When an iOS app advertises in the background, CoreBluetooth moves its service UUIDs out of the standard advertisement field and into an "overflow area" that is only visible to scanners which already know the UUIDs in advance. To work around this, the module exposes a GATT characteristic that holds the device-specific UUIDs, so a scanner that recognizes a shared "network" UUID in the overflow area can connect via GATT to retrieve them. See [Scanning in iOS background mode](#scanning-in-ios-background-mode) for the consumer-side pattern.
|
|
37
|
+
|
|
38
|
+
#### `stopBroadcast(): Promise<{stopped: boolean, stoppedAdvertisers?: number}>`
|
|
39
|
+
|
|
40
|
+
Stop all BLE advertising.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
try {
|
|
44
|
+
const result = await BleAdvertiser.stopBroadcast();
|
|
45
|
+
console.log('Advertising stopped:', result);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('Failed to stop advertising:', error);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
#### `isSupported(): Promise<boolean>`
|
|
52
|
+
|
|
53
|
+
Check if BLE advertising is supported on the current device.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
const supported = await BleAdvertiser.isSupported();
|
|
57
|
+
console.log('BLE advertising supported:', supported);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
#### `isEnabled(): Promise<boolean>`
|
|
61
|
+
|
|
62
|
+
Check if Bluetooth is currently enabled.
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
const enabled = await BleAdvertiser.isEnabled();
|
|
66
|
+
console.log('Bluetooth enabled:', enabled);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Events
|
|
70
|
+
|
|
71
|
+
#### `onAdvertisingStarted`
|
|
72
|
+
|
|
73
|
+
Fired when advertising starts successfully.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { addAdvertisingStartedListener } from '@blunt-ly/ble-advertiser';
|
|
77
|
+
|
|
78
|
+
const subscription = addAdvertisingStartedListener((event) => {
|
|
79
|
+
console.log('Advertising started with UUIDs:', event.uuids);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Don't forget to remove the listener
|
|
83
|
+
subscription.remove();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### `onAdvertisingFailed`
|
|
87
|
+
|
|
88
|
+
Fired when advertising fails to start.
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { addAdvertisingFailedListener } from '@blunt-ly/ble-advertiser';
|
|
92
|
+
|
|
93
|
+
const subscription = addAdvertisingFailedListener((event) => {
|
|
94
|
+
console.log('Advertising failed:', event.error);
|
|
95
|
+
if (event.errorCode) {
|
|
96
|
+
console.log('Error code:', event.errorCode);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### `onAdvertisingStopped`
|
|
102
|
+
|
|
103
|
+
Fired when advertising is stopped.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { addAdvertisingStoppedListener } from '@blunt-ly/ble-advertiser';
|
|
107
|
+
|
|
108
|
+
const subscription = addAdvertisingStoppedListener((event) => {
|
|
109
|
+
console.log('Advertising stopped');
|
|
110
|
+
if (event.uuids) {
|
|
111
|
+
console.log('UUIDs that were being advertised:', event.uuids);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Error Handling
|
|
117
|
+
|
|
118
|
+
The module provides comprehensive error handling with specific error codes and messages:
|
|
119
|
+
|
|
120
|
+
- `BLUETOOTH_NOT_AVAILABLE` - Bluetooth adapter not available
|
|
121
|
+
- `BLUETOOTH_DISABLED` - Bluetooth is turned off
|
|
122
|
+
- `ADVERTISER_NOT_AVAILABLE` - BLE advertiser not supported
|
|
123
|
+
- `INVALID_UUID` - Invalid UUID format provided
|
|
124
|
+
- `NO_UUIDS` - No service UUIDs provided
|
|
125
|
+
- `ADVERTISING_FAILED` - Platform-specific advertising failure
|
|
126
|
+
|
|
127
|
+
## Example Usage
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import React, { useEffect, useState } from 'react';
|
|
131
|
+
import { View, Button, Text, Alert } from 'react-native';
|
|
132
|
+
import * as BleAdvertiser from '@blunt-ly/ble-advertiser';
|
|
133
|
+
|
|
134
|
+
export default function BleAdvertiserExample() {
|
|
135
|
+
const [isAdvertising, setIsAdvertising] = useState(false);
|
|
136
|
+
const [isSupported, setIsSupported] = useState(false);
|
|
137
|
+
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
// Check if BLE advertising is supported
|
|
140
|
+
BleAdvertiser.isSupported().then(setIsSupported);
|
|
141
|
+
|
|
142
|
+
// Set up event listeners
|
|
143
|
+
const startedSubscription = BleAdvertiser.addAdvertisingStartedListener((event) => {
|
|
144
|
+
setIsAdvertising(true);
|
|
145
|
+
Alert.alert('Advertising Started', `UUIDs: ${event.uuids.join(', ')}`);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const failedSubscription = BleAdvertiser.addAdvertisingFailedListener((event) => {
|
|
149
|
+
setIsAdvertising(false);
|
|
150
|
+
Alert.alert('Advertising Failed', event.error);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const stoppedSubscription = BleAdvertiser.addAdvertisingStoppedListener(() => {
|
|
154
|
+
setIsAdvertising(false);
|
|
155
|
+
Alert.alert('Advertising Stopped');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return () => {
|
|
159
|
+
startedSubscription.remove();
|
|
160
|
+
failedSubscription.remove();
|
|
161
|
+
stoppedSubscription.remove();
|
|
162
|
+
};
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
165
|
+
const startAdvertising = async () => {
|
|
166
|
+
try {
|
|
167
|
+
const serviceUUIDs = ['6ba7b810-9dad-11d1-80b4-00c04fd430c8'];
|
|
168
|
+
await BleAdvertiser.broadcast(serviceUUIDs);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
Alert.alert('Error', error.message);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const stopAdvertising = async () => {
|
|
175
|
+
try {
|
|
176
|
+
await BleAdvertiser.stopBroadcast();
|
|
177
|
+
} catch (error) {
|
|
178
|
+
Alert.alert('Error', error.message);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if (!isSupported) {
|
|
183
|
+
return (
|
|
184
|
+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
|
185
|
+
<Text>BLE Advertising is not supported on this device</Text>
|
|
186
|
+
</View>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
|
192
|
+
<Text>BLE Advertising Status: {isAdvertising ? 'Active' : 'Inactive'}</Text>
|
|
193
|
+
<Button
|
|
194
|
+
title={isAdvertising ? 'Stop Advertising' : 'Start Advertising'}
|
|
195
|
+
onPress={isAdvertising ? stopAdvertising : startAdvertising}
|
|
196
|
+
/>
|
|
197
|
+
</View>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Scanning in iOS background mode
|
|
203
|
+
|
|
204
|
+
When the advertising app is backgrounded on iOS, its device-specific service UUIDs only surface in the scanner's `overflowServiceUUIDs` field — and only if the scanner is already scanning for a known "network" UUID that the advertiser also broadcasts. The pattern is:
|
|
205
|
+
|
|
206
|
+
1. The advertiser broadcasts a shared `networkId` UUID plus a device-specific UUID.
|
|
207
|
+
2. The scanner filters on `networkId`.
|
|
208
|
+
3. In the foreground both UUIDs are visible in `advertising.serviceUUIDs`.
|
|
209
|
+
4. In the background only `networkId` is visible (via `overflowServiceUUIDs`); the scanner then connects via GATT to read the device-specific UUID from the characteristic exposed by this module.
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
const NETWORK_ID = '<your-shared-network-uuid>';
|
|
213
|
+
const inFlightConnections = new Set<string>();
|
|
214
|
+
|
|
215
|
+
await scanner.start(async (peripheral) => {
|
|
216
|
+
let targetBroadcastId: string | undefined | null;
|
|
217
|
+
|
|
218
|
+
// Normal path: both service UUIDs are visible in the advertisement
|
|
219
|
+
if (peripheral.advertising.serviceUUIDs?.length === 2) {
|
|
220
|
+
targetBroadcastId = peripheral.advertising.serviceUUIDs.find(
|
|
221
|
+
(uuid) => uuid.toLowerCase() !== NETWORK_ID.toLowerCase()
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Overflow path: only the known network UUID is visible via the overflow area.
|
|
226
|
+
// Connect via GATT to read the device-specific broadcast ID.
|
|
227
|
+
if (!targetBroadcastId) {
|
|
228
|
+
const overflowUUIDs = peripheral.advertising.overflowServiceUUIDs as string[] | undefined;
|
|
229
|
+
const hasNetworkInOverflow = overflowUUIDs?.some(
|
|
230
|
+
(uuid) => uuid.toLowerCase() === NETWORK_ID.toLowerCase()
|
|
231
|
+
);
|
|
232
|
+
if (!hasNetworkInOverflow) return;
|
|
233
|
+
|
|
234
|
+
if (inFlightConnections.has(peripheral.id)) return;
|
|
235
|
+
inFlightConnections.add(peripheral.id);
|
|
236
|
+
|
|
237
|
+
targetBroadcastId = await readBroadcastIdViaGatt(peripheral.id, NETWORK_ID);
|
|
238
|
+
|
|
239
|
+
// Allow re-connection after a cooldown
|
|
240
|
+
setTimeout(() => inFlightConnections.delete(peripheral.id), 60_000);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!targetBroadcastId) return;
|
|
244
|
+
|
|
245
|
+
// Handle the resolved broadcast ID...
|
|
246
|
+
}, [NETWORK_ID]);
|
|
247
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
|
|
3
|
+
group = 'expo.modules.bleadvertiser'
|
|
4
|
+
version = '0.0.0'
|
|
5
|
+
|
|
6
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
7
|
+
apply from: expoModulesCorePlugin
|
|
8
|
+
applyKotlinExpoModulesCorePlugin()
|
|
9
|
+
useCoreDependencies()
|
|
10
|
+
useExpoPublishing()
|
|
11
|
+
|
|
12
|
+
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
|
|
13
|
+
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
|
|
14
|
+
// Most of the time, you may like to manage the Android SDK versions yourself.
|
|
15
|
+
def useManagedAndroidSdkVersions = false
|
|
16
|
+
if (useManagedAndroidSdkVersions) {
|
|
17
|
+
useDefaultAndroidSdkVersions()
|
|
18
|
+
} else {
|
|
19
|
+
buildscript {
|
|
20
|
+
// Simple helper that allows the root project to override versions declared by this library.
|
|
21
|
+
ext.safeExtGet = { prop, fallback ->
|
|
22
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
project.android {
|
|
26
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 36)
|
|
27
|
+
defaultConfig {
|
|
28
|
+
minSdkVersion safeExtGet("minSdkVersion", 24)
|
|
29
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 36)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
android {
|
|
35
|
+
namespace "expo.modules.bleadvertiser"
|
|
36
|
+
defaultConfig {
|
|
37
|
+
versionCode 1
|
|
38
|
+
versionName "0.0.0"
|
|
39
|
+
}
|
|
40
|
+
lintOptions {
|
|
41
|
+
abortOnError false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
package expo.modules.bleadvertiser
|
|
2
|
+
|
|
3
|
+
import android.bluetooth.BluetoothAdapter
|
|
4
|
+
import android.bluetooth.BluetoothManager
|
|
5
|
+
import android.bluetooth.le.AdvertiseCallback
|
|
6
|
+
import android.bluetooth.le.AdvertiseData
|
|
7
|
+
import android.bluetooth.le.AdvertiseSettings
|
|
8
|
+
import android.bluetooth.le.BluetoothLeAdvertiser
|
|
9
|
+
import android.content.Context
|
|
10
|
+
import android.os.ParcelUuid
|
|
11
|
+
import android.util.Log
|
|
12
|
+
import expo.modules.kotlin.Promise
|
|
13
|
+
import expo.modules.kotlin.modules.Module
|
|
14
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
15
|
+
import java.util.UUID
|
|
16
|
+
|
|
17
|
+
class BleAdvertiserModule : Module() {
|
|
18
|
+
private var bluetoothAdapter: BluetoothAdapter? = null
|
|
19
|
+
private val advertisers = mutableMapOf<String, BluetoothLeAdvertiser>()
|
|
20
|
+
private val advertiseCallbacks = mutableMapOf<String, AdvertiseCallback>()
|
|
21
|
+
|
|
22
|
+
override fun definition() = ModuleDefinition {
|
|
23
|
+
Name("BleAdvertiser")
|
|
24
|
+
|
|
25
|
+
Events("onAdvertisingStarted", "onAdvertisingFailed", "onAdvertisingStopped")
|
|
26
|
+
|
|
27
|
+
OnCreate {
|
|
28
|
+
val bluetoothManager = appContext.reactContext?.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
|
|
29
|
+
bluetoothAdapter = bluetoothManager?.adapter
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
AsyncFunction("broadcast") { uuids: List<String>, promise: Promise ->
|
|
33
|
+
try {
|
|
34
|
+
if (bluetoothAdapter == null) {
|
|
35
|
+
promise.reject("BLUETOOTH_NOT_AVAILABLE", "Bluetooth adapter is not available", null)
|
|
36
|
+
return@AsyncFunction
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (bluetoothAdapter?.isEnabled != true) {
|
|
40
|
+
promise.reject("BLUETOOTH_DISABLED", "Bluetooth is not enabled", null)
|
|
41
|
+
return@AsyncFunction
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
val advertiser = bluetoothAdapter?.bluetoothLeAdvertiser
|
|
45
|
+
if (advertiser == null) {
|
|
46
|
+
promise.reject("ADVERTISER_NOT_AVAILABLE", "BLE advertiser is not available on this device", null)
|
|
47
|
+
return@AsyncFunction
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Convert string UUIDs to ParcelUuid objects
|
|
51
|
+
val serviceUuids = mutableListOf<ParcelUuid>()
|
|
52
|
+
for (uuidString in uuids) {
|
|
53
|
+
try {
|
|
54
|
+
val uuid = UUID.fromString(uuidString)
|
|
55
|
+
serviceUuids.add(ParcelUuid(uuid))
|
|
56
|
+
} catch (e: IllegalArgumentException) {
|
|
57
|
+
promise.reject("INVALID_UUID", "Invalid UUID format: $uuidString", e)
|
|
58
|
+
return@AsyncFunction
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (serviceUuids.isEmpty()) {
|
|
63
|
+
promise.reject("NO_UUIDS", "At least one service UUID is required", null)
|
|
64
|
+
return@AsyncFunction
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Build advertise settings
|
|
68
|
+
val settings = AdvertiseSettings.Builder()
|
|
69
|
+
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
|
|
70
|
+
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
|
|
71
|
+
.setConnectable(false)
|
|
72
|
+
.build()
|
|
73
|
+
|
|
74
|
+
// Build advertise data with service UUIDs
|
|
75
|
+
val dataBuilder = AdvertiseData.Builder()
|
|
76
|
+
.setIncludeDeviceName(false)
|
|
77
|
+
.setIncludeTxPowerLevel(true)
|
|
78
|
+
|
|
79
|
+
// Add all service UUIDs
|
|
80
|
+
for (serviceUuid in serviceUuids) {
|
|
81
|
+
dataBuilder.addServiceUuid(serviceUuid)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
val advertiseData = dataBuilder.build()
|
|
85
|
+
|
|
86
|
+
val callback = object : AdvertiseCallback() {
|
|
87
|
+
override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) {
|
|
88
|
+
Log.d("BleAdvertiser", "Advertising started successfully")
|
|
89
|
+
sendEvent("onAdvertisingStarted", mapOf("uuids" to uuids))
|
|
90
|
+
promise.resolve("Advertising started successfully")
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
override fun onStartFailure(errorCode: Int) {
|
|
94
|
+
val errorMessage = when (errorCode) {
|
|
95
|
+
ADVERTISE_FAILED_FEATURE_UNSUPPORTED -> "Advertising feature is not supported"
|
|
96
|
+
ADVERTISE_FAILED_TOO_MANY_ADVERTISERS -> "Too many advertisers"
|
|
97
|
+
ADVERTISE_FAILED_ALREADY_STARTED -> "Advertising already started"
|
|
98
|
+
ADVERTISE_FAILED_DATA_TOO_LARGE -> "Advertise data too large"
|
|
99
|
+
ADVERTISE_FAILED_INTERNAL_ERROR -> "Internal error"
|
|
100
|
+
else -> "Unknown error code: $errorCode"
|
|
101
|
+
}
|
|
102
|
+
Log.e("BleAdvertiser", "Advertising failed: $errorMessage")
|
|
103
|
+
sendEvent("onAdvertisingFailed", mapOf("error" to errorMessage, "errorCode" to errorCode))
|
|
104
|
+
promise.reject("ADVERTISING_FAILED", errorMessage, null)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Generate a key for this advertiser instance
|
|
109
|
+
val key = uuids.sorted().joinToString(",")
|
|
110
|
+
|
|
111
|
+
// Stop any existing advertiser with the same UUIDs
|
|
112
|
+
advertisers[key]?.let { existingAdvertiser ->
|
|
113
|
+
advertiseCallbacks[key]?.let { existingCallback ->
|
|
114
|
+
existingAdvertiser.stopAdvertising(existingCallback)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Store the new advertiser and callback
|
|
119
|
+
advertisers[key] = advertiser
|
|
120
|
+
advertiseCallbacks[key] = callback
|
|
121
|
+
|
|
122
|
+
// Start advertising
|
|
123
|
+
advertiser.startAdvertising(settings, advertiseData, callback)
|
|
124
|
+
|
|
125
|
+
} catch (e: Exception) {
|
|
126
|
+
Log.e("BleAdvertiser", "Error starting advertising", e)
|
|
127
|
+
promise.reject("UNEXPECTED_ERROR", e.message ?: "Unknown error", e)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
AsyncFunction("stopBroadcast") { promise: Promise ->
|
|
132
|
+
try {
|
|
133
|
+
val stoppedKeys = mutableListOf<String>()
|
|
134
|
+
|
|
135
|
+
for ((key, advertiser) in advertisers) {
|
|
136
|
+
advertiseCallbacks[key]?.let { callback ->
|
|
137
|
+
advertiser.stopAdvertising(callback)
|
|
138
|
+
stoppedKeys.add(key)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
advertisers.clear()
|
|
143
|
+
advertiseCallbacks.clear()
|
|
144
|
+
|
|
145
|
+
sendEvent("onAdvertisingStopped", mapOf("stoppedCount" to stoppedKeys.size))
|
|
146
|
+
promise.resolve(mapOf("stoppedAdvertisers" to stoppedKeys.size))
|
|
147
|
+
|
|
148
|
+
} catch (e: Exception) {
|
|
149
|
+
Log.e("BleAdvertiser", "Error stopping advertising", e)
|
|
150
|
+
promise.reject("STOP_ERROR", e.message ?: "Unknown error", e)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
AsyncFunction("isSupported") { promise: Promise ->
|
|
155
|
+
try {
|
|
156
|
+
val isSupported = bluetoothAdapter != null &&
|
|
157
|
+
bluetoothAdapter?.bluetoothLeAdvertiser != null
|
|
158
|
+
promise.resolve(isSupported)
|
|
159
|
+
} catch (e: Exception) {
|
|
160
|
+
promise.reject("CHECK_SUPPORT_ERROR", e.message ?: "Unknown error", e)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
AsyncFunction("isEnabled") { promise: Promise ->
|
|
165
|
+
try {
|
|
166
|
+
val isEnabled = bluetoothAdapter?.isEnabled == true
|
|
167
|
+
promise.resolve(isEnabled)
|
|
168
|
+
} catch (e: Exception) {
|
|
169
|
+
promise.reject("CHECK_ENABLED_ERROR", e.message ?: "Unknown error", e)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type AdvertisingStartedEvent = {
|
|
2
|
+
uuids: string[];
|
|
3
|
+
};
|
|
4
|
+
export type AdvertisingFailedEvent = {
|
|
5
|
+
error: string;
|
|
6
|
+
errorCode?: number;
|
|
7
|
+
uuids?: string[];
|
|
8
|
+
};
|
|
9
|
+
export type AdvertisingStoppedEvent = {
|
|
10
|
+
uuids?: string[];
|
|
11
|
+
stoppedCount?: number;
|
|
12
|
+
};
|
|
13
|
+
export type BleAdvertiserModuleEvents = {
|
|
14
|
+
onAdvertisingStarted: (params: AdvertisingStartedEvent) => void;
|
|
15
|
+
onAdvertisingFailed: (params: AdvertisingFailedEvent) => void;
|
|
16
|
+
onAdvertisingStopped: (params: AdvertisingStoppedEvent) => void;
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=BleAdvertiser.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BleAdvertiser.types.d.ts","sourceRoot":"","sources":["../src/BleAdvertiser.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,oBAAoB,EAAE,CAAC,MAAM,EAAE,uBAAuB,KAAK,IAAI,CAAC;IAChE,mBAAmB,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,IAAI,CAAC;IAC9D,oBAAoB,EAAE,CAAC,MAAM,EAAE,uBAAuB,KAAK,IAAI,CAAC;CACjE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BleAdvertiser.types.js","sourceRoot":"","sources":["../src/BleAdvertiser.types.ts"],"names":[],"mappings":"","sourcesContent":["export type AdvertisingStartedEvent = {\n uuids: string[];\n};\n\nexport type AdvertisingFailedEvent = {\n error: string;\n errorCode?: number;\n uuids?: string[];\n};\n\nexport type AdvertisingStoppedEvent = {\n uuids?: string[];\n stoppedCount?: number;\n};\n\nexport type BleAdvertiserModuleEvents = {\n onAdvertisingStarted: (params: AdvertisingStartedEvent) => void;\n onAdvertisingFailed: (params: AdvertisingFailedEvent) => void;\n onAdvertisingStopped: (params: AdvertisingStoppedEvent) => void;\n};\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { NativeModule } from 'expo';
|
|
2
|
+
import { BleAdvertiserModuleEvents } from './BleAdvertiser.types';
|
|
3
|
+
declare class BleAdvertiserModule extends NativeModule<BleAdvertiserModuleEvents> {
|
|
4
|
+
/**
|
|
5
|
+
* Start BLE advertising with the specified service UUIDs
|
|
6
|
+
* @param uuids Array of service UUID strings to advertise
|
|
7
|
+
* @returns Promise that resolves when advertising starts successfully
|
|
8
|
+
*/
|
|
9
|
+
broadcast(uuids: string[]): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Stop all BLE advertising
|
|
12
|
+
* @returns Promise that resolves with information about stopped advertisers
|
|
13
|
+
*/
|
|
14
|
+
stopBroadcast(): Promise<{
|
|
15
|
+
stopped: boolean;
|
|
16
|
+
stoppedAdvertisers?: number;
|
|
17
|
+
}>;
|
|
18
|
+
/**
|
|
19
|
+
* Check if BLE advertising is supported on this device
|
|
20
|
+
* @returns Promise that resolves to true if supported, false otherwise
|
|
21
|
+
*/
|
|
22
|
+
isSupported(): Promise<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Check if Bluetooth is currently enabled
|
|
25
|
+
* @returns Promise that resolves to true if enabled, false otherwise
|
|
26
|
+
*/
|
|
27
|
+
isEnabled(): Promise<boolean>;
|
|
28
|
+
}
|
|
29
|
+
declare const _default: BleAdvertiserModule;
|
|
30
|
+
export default _default;
|
|
31
|
+
//# sourceMappingURL=BleAdvertiserModule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BleAdvertiserModule.d.ts","sourceRoot":"","sources":["../src/BleAdvertiserModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAElE,OAAO,OAAO,mBAAoB,SAAQ,YAAY,CAAC,yBAAyB,CAAC;IAC/E;;;;OAIG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAE3C;;;OAGG;IACH,aAAa,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAE3E;;;OAGG;IACH,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAE/B;;;OAGG;IACH,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;CAC9B;;AAGD,wBAAyE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BleAdvertiserModule.js","sourceRoot":"","sources":["../src/BleAdvertiserModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AA+BzD,yDAAyD;AACzD,eAAe,mBAAmB,CAAsB,eAAe,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from 'expo';\n\nimport { BleAdvertiserModuleEvents } from './BleAdvertiser.types';\n\ndeclare class BleAdvertiserModule extends NativeModule<BleAdvertiserModuleEvents> {\n /**\n * Start BLE advertising with the specified service UUIDs\n * @param uuids Array of service UUID strings to advertise\n * @returns Promise that resolves when advertising starts successfully\n */\n broadcast(uuids: string[]): Promise<string>;\n\n /**\n * Stop all BLE advertising\n * @returns Promise that resolves with information about stopped advertisers\n */\n stopBroadcast(): Promise<{ stopped: boolean; stoppedAdvertisers?: number }>;\n\n /**\n * Check if BLE advertising is supported on this device\n * @returns Promise that resolves to true if supported, false otherwise\n */\n isSupported(): Promise<boolean>;\n\n /**\n * Check if Bluetooth is currently enabled\n * @returns Promise that resolves to true if enabled, false otherwise\n */\n isEnabled(): Promise<boolean>;\n}\n\n// This call loads the native module object from the JSI.\nexport default requireNativeModule<BleAdvertiserModule>('BleAdvertiser');\n"]}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { EventSubscription } from 'expo-modules-core';
|
|
2
|
+
import { AdvertisingStartedEvent, AdvertisingFailedEvent, AdvertisingStoppedEvent } from './BleAdvertiser.types';
|
|
3
|
+
/**
|
|
4
|
+
* Start BLE advertising with the specified service UUIDs
|
|
5
|
+
* @param uuids Array of service UUID strings to advertise
|
|
6
|
+
* @returns Promise that resolves when advertising starts successfully
|
|
7
|
+
*/
|
|
8
|
+
export declare function broadcast(uuids: string[]): Promise<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Stop all BLE advertising
|
|
11
|
+
* @returns Promise that resolves with information about stopped advertisers
|
|
12
|
+
*/
|
|
13
|
+
export declare function stopBroadcast(): Promise<{
|
|
14
|
+
stopped: boolean;
|
|
15
|
+
stoppedAdvertisers?: number;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* Check if BLE advertising is supported on this device
|
|
19
|
+
* @returns Promise that resolves to true if supported, false otherwise
|
|
20
|
+
*/
|
|
21
|
+
export declare function isSupported(): Promise<boolean>;
|
|
22
|
+
/**
|
|
23
|
+
* Check if Bluetooth is currently enabled
|
|
24
|
+
* @returns Promise that resolves to true if enabled, false otherwise
|
|
25
|
+
*/
|
|
26
|
+
export declare function isEnabled(): Promise<boolean>;
|
|
27
|
+
/**
|
|
28
|
+
* Add listener for advertising started events
|
|
29
|
+
* @param listener Function to call when advertising starts
|
|
30
|
+
* @returns EventSubscription that can be used to remove the listener
|
|
31
|
+
*/
|
|
32
|
+
export declare function addAdvertisingStartedListener(listener: (event: AdvertisingStartedEvent) => void): EventSubscription;
|
|
33
|
+
/**
|
|
34
|
+
* Add listener for advertising failed events
|
|
35
|
+
* @param listener Function to call when advertising fails
|
|
36
|
+
* @returns EventSubscription that can be used to remove the listener
|
|
37
|
+
*/
|
|
38
|
+
export declare function addAdvertisingFailedListener(listener: (event: AdvertisingFailedEvent) => void): EventSubscription;
|
|
39
|
+
/**
|
|
40
|
+
* Add listener for advertising stopped events
|
|
41
|
+
* @param listener Function to call when advertising stops
|
|
42
|
+
* @returns EventSubscription that can be used to remove the listener
|
|
43
|
+
*/
|
|
44
|
+
export declare function addAdvertisingStoppedListener(listener: (event: AdvertisingStoppedEvent) => void): EventSubscription;
|
|
45
|
+
export * from './BleAdvertiser.types';
|
|
46
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGtD,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACxB,MAAM,uBAAuB,CAAC;AAe/B;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAE1D;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAE1F;AAED;;;GAGG;AACH,wBAAgB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAE9C;AAED;;;GAGG;AACH,wBAAgB,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,CAE5C;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,GACjD,iBAAiB,CAEnB;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,GAChD,iBAAiB,CAEnB;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,GACjD,iBAAiB,CAEnB;AAGD,cAAc,uBAAuB,CAAC"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import BleAdvertiserModule from './BleAdvertiserModule';
|
|
3
|
+
import { expand16BitUuid } from './utils';
|
|
4
|
+
// HACK: Android's BluetoothLeAdvertiser only accepts 128-bit service UUIDs, while
|
|
5
|
+
// CoreBluetooth on iOS accepts the 16-bit short form directly. Expand any
|
|
6
|
+
// 16-bit input before handing it to the Android native module.
|
|
7
|
+
function normalizeUuidsForPlatform(uuids) {
|
|
8
|
+
if (Platform.OS !== 'android')
|
|
9
|
+
return uuids;
|
|
10
|
+
return uuids.map((uuid) => {
|
|
11
|
+
const hex = uuid.replace(/[^0-9a-fA-F]/g, '');
|
|
12
|
+
return hex.length === 4 ? expand16BitUuid(uuid) : uuid;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Start BLE advertising with the specified service UUIDs
|
|
17
|
+
* @param uuids Array of service UUID strings to advertise
|
|
18
|
+
* @returns Promise that resolves when advertising starts successfully
|
|
19
|
+
*/
|
|
20
|
+
export function broadcast(uuids) {
|
|
21
|
+
return BleAdvertiserModule.broadcast(normalizeUuidsForPlatform(uuids));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Stop all BLE advertising
|
|
25
|
+
* @returns Promise that resolves with information about stopped advertisers
|
|
26
|
+
*/
|
|
27
|
+
export function stopBroadcast() {
|
|
28
|
+
return BleAdvertiserModule.stopBroadcast();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Check if BLE advertising is supported on this device
|
|
32
|
+
* @returns Promise that resolves to true if supported, false otherwise
|
|
33
|
+
*/
|
|
34
|
+
export function isSupported() {
|
|
35
|
+
return BleAdvertiserModule.isSupported();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Check if Bluetooth is currently enabled
|
|
39
|
+
* @returns Promise that resolves to true if enabled, false otherwise
|
|
40
|
+
*/
|
|
41
|
+
export function isEnabled() {
|
|
42
|
+
return BleAdvertiserModule.isEnabled();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Add listener for advertising started events
|
|
46
|
+
* @param listener Function to call when advertising starts
|
|
47
|
+
* @returns EventSubscription that can be used to remove the listener
|
|
48
|
+
*/
|
|
49
|
+
export function addAdvertisingStartedListener(listener) {
|
|
50
|
+
return BleAdvertiserModule.addListener('onAdvertisingStarted', listener);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Add listener for advertising failed events
|
|
54
|
+
* @param listener Function to call when advertising fails
|
|
55
|
+
* @returns EventSubscription that can be used to remove the listener
|
|
56
|
+
*/
|
|
57
|
+
export function addAdvertisingFailedListener(listener) {
|
|
58
|
+
return BleAdvertiserModule.addListener('onAdvertisingFailed', listener);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Add listener for advertising stopped events
|
|
62
|
+
* @param listener Function to call when advertising stops
|
|
63
|
+
* @returns EventSubscription that can be used to remove the listener
|
|
64
|
+
*/
|
|
65
|
+
export function addAdvertisingStoppedListener(listener) {
|
|
66
|
+
return BleAdvertiserModule.addListener('onAdvertisingStopped', listener);
|
|
67
|
+
}
|
|
68
|
+
// Export the types for consumers
|
|
69
|
+
export * from './BleAdvertiser.types';
|
|
70
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAOxC,OAAO,mBAAmB,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE1C,kFAAkF;AAClF,0EAA0E;AAC1E,+DAA+D;AAC/D,SAAS,yBAAyB,CAAC,KAAe;IAChD,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAC5C,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACxB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;QAC9C,OAAO,GAAG,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACzD,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,KAAe;IACvC,OAAO,mBAAmB,CAAC,SAAS,CAAC,yBAAyB,CAAC,KAAK,CAAC,CAAC,CAAC;AACzE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,mBAAmB,CAAC,aAAa,EAAE,CAAC;AAC7C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW;IACzB,OAAO,mBAAmB,CAAC,WAAW,EAAE,CAAC;AAC3C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS;IACvB,OAAO,mBAAmB,CAAC,SAAS,EAAE,CAAC;AACzC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAC3C,QAAkD;IAElD,OAAO,mBAAmB,CAAC,WAAW,CAAC,sBAAsB,EAAE,QAAQ,CAAC,CAAC;AAC3E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,4BAA4B,CAC1C,QAAiD;IAEjD,OAAO,mBAAmB,CAAC,WAAW,CAAC,qBAAqB,EAAE,QAAQ,CAAC,CAAC;AAC1E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAC3C,QAAkD;IAElD,OAAO,mBAAmB,CAAC,WAAW,CAAC,sBAAsB,EAAE,QAAQ,CAAC,CAAC;AAC3E,CAAC;AAED,iCAAiC;AACjC,cAAc,uBAAuB,CAAC","sourcesContent":["import { EventSubscription } from 'expo-modules-core';\nimport { Platform } from 'react-native';\n\nimport {\n AdvertisingStartedEvent,\n AdvertisingFailedEvent,\n AdvertisingStoppedEvent,\n} from './BleAdvertiser.types';\nimport BleAdvertiserModule from './BleAdvertiserModule';\nimport { expand16BitUuid } from './utils';\n\n// HACK: Android's BluetoothLeAdvertiser only accepts 128-bit service UUIDs, while\n// CoreBluetooth on iOS accepts the 16-bit short form directly. Expand any\n// 16-bit input before handing it to the Android native module.\nfunction normalizeUuidsForPlatform(uuids: string[]): string[] {\n if (Platform.OS !== 'android') return uuids;\n return uuids.map((uuid) => {\n const hex = uuid.replace(/[^0-9a-fA-F]/g, '');\n return hex.length === 4 ? expand16BitUuid(uuid) : uuid;\n });\n}\n\n/**\n * Start BLE advertising with the specified service UUIDs\n * @param uuids Array of service UUID strings to advertise\n * @returns Promise that resolves when advertising starts successfully\n */\nexport function broadcast(uuids: string[]): Promise<string> {\n return BleAdvertiserModule.broadcast(normalizeUuidsForPlatform(uuids));\n}\n\n/**\n * Stop all BLE advertising\n * @returns Promise that resolves with information about stopped advertisers\n */\nexport function stopBroadcast(): Promise<{ stopped: boolean; stoppedAdvertisers?: number }> {\n return BleAdvertiserModule.stopBroadcast();\n}\n\n/**\n * Check if BLE advertising is supported on this device\n * @returns Promise that resolves to true if supported, false otherwise\n */\nexport function isSupported(): Promise<boolean> {\n return BleAdvertiserModule.isSupported();\n}\n\n/**\n * Check if Bluetooth is currently enabled\n * @returns Promise that resolves to true if enabled, false otherwise\n */\nexport function isEnabled(): Promise<boolean> {\n return BleAdvertiserModule.isEnabled();\n}\n\n/**\n * Add listener for advertising started events\n * @param listener Function to call when advertising starts\n * @returns EventSubscription that can be used to remove the listener\n */\nexport function addAdvertisingStartedListener(\n listener: (event: AdvertisingStartedEvent) => void\n): EventSubscription {\n return BleAdvertiserModule.addListener('onAdvertisingStarted', listener);\n}\n\n/**\n * Add listener for advertising failed events\n * @param listener Function to call when advertising fails\n * @returns EventSubscription that can be used to remove the listener\n */\nexport function addAdvertisingFailedListener(\n listener: (event: AdvertisingFailedEvent) => void\n): EventSubscription {\n return BleAdvertiserModule.addListener('onAdvertisingFailed', listener);\n}\n\n/**\n * Add listener for advertising stopped events\n * @param listener Function to call when advertising stops\n * @returns EventSubscription that can be used to remove the listener\n */\nexport function addAdvertisingStoppedListener(\n listener: (event: AdvertisingStoppedEvent) => void\n): EventSubscription {\n return BleAdvertiserModule.addListener('onAdvertisingStopped', listener);\n}\n\n// Export the types for consumers\nexport * from './BleAdvertiser.types';\n"]}
|
package/build/utils.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expands a 16-bit UUID to the full 128-bit Bluetooth UUID format.
|
|
3
|
+
*
|
|
4
|
+
* @param shortUuid 16-bit UUID string (with or without dashes)
|
|
5
|
+
* @returns Expanded 128-bit UUID string
|
|
6
|
+
* @throws Error if the input is not a valid 16-bit UUID
|
|
7
|
+
*/
|
|
8
|
+
export declare function expand16BitUuid(shortUuid: string): string;
|
|
9
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAazD"}
|
package/build/utils.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expands a 16-bit UUID to the full 128-bit Bluetooth UUID format.
|
|
3
|
+
*
|
|
4
|
+
* @param shortUuid 16-bit UUID string (with or without dashes)
|
|
5
|
+
* @returns Expanded 128-bit UUID string
|
|
6
|
+
* @throws Error if the input is not a valid 16-bit UUID
|
|
7
|
+
*/
|
|
8
|
+
export function expand16BitUuid(shortUuid) {
|
|
9
|
+
// Clean and validate input
|
|
10
|
+
const cleaned = shortUuid.replace(/[^0-9a-fA-F]/g, '');
|
|
11
|
+
if (!/^[0-9a-fA-F]{4}$/i.test(cleaned)) {
|
|
12
|
+
throw new Error(`Invalid 16-bit UUID: ${shortUuid}`);
|
|
13
|
+
}
|
|
14
|
+
// Pad to 4 chars and convert to lowercase
|
|
15
|
+
const paddedUuid = cleaned.padStart(4, '0').toLowerCase();
|
|
16
|
+
// Bluetooth Base UUID: 00000000-0000-1000-8000-00805F9B34FB
|
|
17
|
+
return `0000${paddedUuid}-0000-1000-8000-00805f9b34fb`;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,SAAiB;IAC/C,2BAA2B;IAC3B,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;IAEvD,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,wBAAwB,SAAS,EAAE,CAAC,CAAC;IACvD,CAAC;IAED,0CAA0C;IAC1C,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;IAE1D,4DAA4D;IAC5D,OAAO,OAAO,UAAU,8BAA8B,CAAC;AACzD,CAAC","sourcesContent":["/**\n * Expands a 16-bit UUID to the full 128-bit Bluetooth UUID format.\n *\n * @param shortUuid 16-bit UUID string (with or without dashes)\n * @returns Expanded 128-bit UUID string\n * @throws Error if the input is not a valid 16-bit UUID\n */\nexport function expand16BitUuid(shortUuid: string): string {\n // Clean and validate input\n const cleaned = shortUuid.replace(/[^0-9a-fA-F]/g, '');\n\n if (!/^[0-9a-fA-F]{4}$/i.test(cleaned)) {\n throw new Error(`Invalid 16-bit UUID: ${shortUuid}`);\n }\n\n // Pad to 4 chars and convert to lowercase\n const paddedUuid = cleaned.padStart(4, '0').toLowerCase();\n\n // Bluetooth Base UUID: 00000000-0000-1000-8000-00805F9B34FB\n return `0000${paddedUuid}-0000-1000-8000-00805f9b34fb`;\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Pod::Spec.new do |s|
|
|
2
|
+
s.name = 'BleAdvertiser'
|
|
3
|
+
s.version = '0.0.0'
|
|
4
|
+
s.summary = 'Minimal BLE advertiser for React-Native, optimized for background processing.'
|
|
5
|
+
s.author = 'The Blunt.ly Authors'
|
|
6
|
+
s.homepage = 'https://github.com/Blunt-ly/ble-advertiser'
|
|
7
|
+
s.platforms = {
|
|
8
|
+
:ios => '15.1',
|
|
9
|
+
:tvos => '15.1'
|
|
10
|
+
}
|
|
11
|
+
s.source = { git: '' }
|
|
12
|
+
s.static_framework = true
|
|
13
|
+
|
|
14
|
+
s.dependency 'ExpoModulesCore'
|
|
15
|
+
|
|
16
|
+
# Swift/Objective-C compatibility
|
|
17
|
+
s.pod_target_xcconfig = {
|
|
18
|
+
'DEFINES_MODULE' => 'YES',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
|
22
|
+
end
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import CoreBluetooth
|
|
3
|
+
|
|
4
|
+
// The GATT characteristic UUID reuses the network UUID (first element passed to broadcast()).
|
|
5
|
+
// CoreBluetooth distinguishes services and characteristics by their role in the GATT hierarchy.
|
|
6
|
+
|
|
7
|
+
private class BlePeripheralManagerDelegate: NSObject, CBPeripheralManagerDelegate {
|
|
8
|
+
weak var module: BleAdvertiserModule?
|
|
9
|
+
|
|
10
|
+
init(module: BleAdvertiserModule) {
|
|
11
|
+
self.module = module
|
|
12
|
+
super.init()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
|
|
16
|
+
module?.handlePeripheralManagerDidUpdateState(peripheral)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
|
|
20
|
+
module?.handlePeripheralManagerDidStartAdvertising(peripheral, error: error)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
|
|
24
|
+
module?.handlePeripheralManagerDidAddService(peripheral, service: service, error: error)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
|
|
28
|
+
module?.handlePeripheralManagerDidReceiveRead(peripheral, request: request)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public class BleAdvertiserModule: Module {
|
|
33
|
+
private var peripheralManager: CBPeripheralManager?
|
|
34
|
+
private var delegate: BlePeripheralManagerDelegate?
|
|
35
|
+
private var isAdvertising = false
|
|
36
|
+
private var currentAdvertisingUUIDs: [String] = []
|
|
37
|
+
private var gattService: CBMutableService?
|
|
38
|
+
private var broadcastIdCharacteristic: CBMutableCharacteristic?
|
|
39
|
+
private var currentBroadcastId: String?
|
|
40
|
+
|
|
41
|
+
public func definition() -> ModuleDefinition {
|
|
42
|
+
Name("BleAdvertiser")
|
|
43
|
+
|
|
44
|
+
Events("onAdvertisingStarted", "onAdvertisingFailed", "onAdvertisingStopped")
|
|
45
|
+
|
|
46
|
+
OnCreate {
|
|
47
|
+
self.delegate = BlePeripheralManagerDelegate(module: self)
|
|
48
|
+
self.peripheralManager = CBPeripheralManager(delegate: self.delegate, queue: nil)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
AsyncFunction("broadcast") { (uuids: [String], promise: Promise) in
|
|
52
|
+
guard let peripheralManager = self.peripheralManager else {
|
|
53
|
+
promise.reject("PERIPHERAL_MANAGER_NOT_AVAILABLE", "Peripheral manager is not available")
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
guard peripheralManager.state == .poweredOn else {
|
|
58
|
+
let stateDescription = self.bluetoothStateDescription(peripheralManager.state)
|
|
59
|
+
promise.reject("BLUETOOTH_NOT_READY", "Bluetooth state: \(stateDescription)")
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
guard !uuids.isEmpty else {
|
|
64
|
+
promise.reject("NO_UUIDS", "At least one service UUID is required")
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Stop any existing advertising and remove existing GATT services
|
|
69
|
+
if self.isAdvertising {
|
|
70
|
+
peripheralManager.stopAdvertising()
|
|
71
|
+
self.isAdvertising = false
|
|
72
|
+
}
|
|
73
|
+
if self.gattService != nil {
|
|
74
|
+
peripheralManager.removeAllServices()
|
|
75
|
+
self.gattService = nil
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Convert string UUIDs to CBUUID objects
|
|
79
|
+
var serviceUUIDs: [CBUUID] = []
|
|
80
|
+
for uuidString in uuids {
|
|
81
|
+
let cbuuid = CBUUID(string: uuidString)
|
|
82
|
+
serviceUUIDs.append(cbuuid)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
self.currentAdvertisingUUIDs = uuids
|
|
86
|
+
|
|
87
|
+
// Identify the network UUID (first) and broadcast ID (second) from the uuids array.
|
|
88
|
+
// Register a GATT service under the network UUID with a readable characteristic
|
|
89
|
+
// that returns the broadcast ID. This allows centrals that discover us via the
|
|
90
|
+
// overflow area (where only the known network UUID is visible) to connect and
|
|
91
|
+
// retrieve our device-specific broadcast ID through GATT.
|
|
92
|
+
if uuids.count >= 2 {
|
|
93
|
+
let networkUUID = CBUUID(string: uuids[0])
|
|
94
|
+
self.currentBroadcastId = uuids[1]
|
|
95
|
+
|
|
96
|
+
self.broadcastIdCharacteristic = CBMutableCharacteristic(
|
|
97
|
+
type: networkUUID,
|
|
98
|
+
properties: [.read],
|
|
99
|
+
value: nil, // nil = dynamic, served via didReceiveRead delegate
|
|
100
|
+
permissions: [.readable]
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
let service = CBMutableService(type: networkUUID, primary: true)
|
|
104
|
+
service.characteristics = [self.broadcastIdCharacteristic!]
|
|
105
|
+
self.gattService = service
|
|
106
|
+
|
|
107
|
+
// Adding the service triggers didAdd delegate; advertising starts after that
|
|
108
|
+
self.broadcastPromise = promise
|
|
109
|
+
peripheralManager.add(service)
|
|
110
|
+
} else {
|
|
111
|
+
// Single UUID — no GATT service needed, just advertise directly
|
|
112
|
+
let advertisementData: [String: Any] = [CBAdvertisementDataServiceUUIDsKey: serviceUUIDs]
|
|
113
|
+
peripheralManager.startAdvertising(advertisementData)
|
|
114
|
+
self.broadcastPromise = promise
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
AsyncFunction("stopBroadcast") { (promise: Promise) in
|
|
119
|
+
guard let peripheralManager = self.peripheralManager else {
|
|
120
|
+
promise.reject("PERIPHERAL_MANAGER_NOT_AVAILABLE", "Peripheral manager is not available")
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if self.isAdvertising {
|
|
125
|
+
peripheralManager.stopAdvertising()
|
|
126
|
+
self.isAdvertising = false
|
|
127
|
+
self.sendEvent("onAdvertisingStopped", ["uuids": self.currentAdvertisingUUIDs])
|
|
128
|
+
self.currentAdvertisingUUIDs = []
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if self.gattService != nil {
|
|
132
|
+
peripheralManager.removeAllServices()
|
|
133
|
+
self.gattService = nil
|
|
134
|
+
self.broadcastIdCharacteristic = nil
|
|
135
|
+
self.currentBroadcastId = nil
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
promise.resolve(["stopped": self.isAdvertising == false])
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
AsyncFunction("isSupported") { (promise: Promise) in
|
|
142
|
+
let isSupported = CBPeripheralManager.authorization == .allowedAlways ||
|
|
143
|
+
CBPeripheralManager.authorization == .notDetermined
|
|
144
|
+
promise.resolve(isSupported)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
AsyncFunction("isEnabled") { (promise: Promise) in
|
|
148
|
+
guard let peripheralManager = self.peripheralManager else {
|
|
149
|
+
promise.resolve(false)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let isEnabled = peripheralManager.state == .poweredOn
|
|
154
|
+
promise.resolve(isEnabled)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private var broadcastPromise: Promise?
|
|
159
|
+
|
|
160
|
+
// MARK: - Delegate Handler Methods
|
|
161
|
+
|
|
162
|
+
func handlePeripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
|
|
163
|
+
let stateDescription = bluetoothStateDescription(peripheral.state)
|
|
164
|
+
print("BleAdvertiser: Peripheral manager state changed to \(stateDescription)")
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
func handlePeripheralManagerDidAddService(_ peripheral: CBPeripheralManager, service: CBService, error: Error?) {
|
|
168
|
+
if let error = error {
|
|
169
|
+
print("BleAdvertiser: Failed to add GATT service: \(error.localizedDescription)")
|
|
170
|
+
self.broadcastPromise?.reject("GATT_SERVICE_FAILED", error.localizedDescription)
|
|
171
|
+
self.broadcastPromise = nil
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
print("BleAdvertiser: GATT service added, starting advertisement")
|
|
176
|
+
|
|
177
|
+
var serviceUUIDs: [CBUUID] = []
|
|
178
|
+
for uuidString in self.currentAdvertisingUUIDs {
|
|
179
|
+
serviceUUIDs.append(CBUUID(string: uuidString))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let advertisementData: [String: Any] = [CBAdvertisementDataServiceUUIDsKey: serviceUUIDs]
|
|
183
|
+
peripheral.startAdvertising(advertisementData)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
func handlePeripheralManagerDidReceiveRead(_ peripheral: CBPeripheralManager, request: CBATTRequest) {
|
|
187
|
+
guard request.characteristic.uuid == self.broadcastIdCharacteristic?.uuid else {
|
|
188
|
+
peripheral.respond(to: request, withResult: .attributeNotFound)
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
guard let broadcastId = self.currentBroadcastId,
|
|
193
|
+
let data = broadcastId.data(using: .utf8) else {
|
|
194
|
+
peripheral.respond(to: request, withResult: .unlikelyError)
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
guard request.offset <= data.count else {
|
|
199
|
+
peripheral.respond(to: request, withResult: .invalidOffset)
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
request.value = data.subdata(in: request.offset..<data.count)
|
|
204
|
+
peripheral.respond(to: request, withResult: .success)
|
|
205
|
+
print("BleAdvertiser: Responded to GATT read request with broadcastId: \(broadcastId)")
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
func handlePeripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
|
|
209
|
+
if let error = error {
|
|
210
|
+
print("BleAdvertiser: Failed to start advertising: \(error.localizedDescription)")
|
|
211
|
+
self.isAdvertising = false
|
|
212
|
+
self.sendEvent("onAdvertisingFailed", [
|
|
213
|
+
"error": error.localizedDescription,
|
|
214
|
+
"uuids": self.currentAdvertisingUUIDs
|
|
215
|
+
])
|
|
216
|
+
self.broadcastPromise?.reject("ADVERTISING_FAILED", error.localizedDescription)
|
|
217
|
+
} else {
|
|
218
|
+
print("BleAdvertiser: Successfully started advertising")
|
|
219
|
+
self.isAdvertising = true
|
|
220
|
+
self.sendEvent("onAdvertisingStarted", ["uuids": self.currentAdvertisingUUIDs])
|
|
221
|
+
self.broadcastPromise?.resolve("Advertising started successfully")
|
|
222
|
+
}
|
|
223
|
+
self.broadcastPromise = nil
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// MARK: - Helper Methods
|
|
227
|
+
|
|
228
|
+
private func bluetoothStateDescription(_ state: CBManagerState) -> String {
|
|
229
|
+
switch state {
|
|
230
|
+
case .unknown:
|
|
231
|
+
return "unknown"
|
|
232
|
+
case .resetting:
|
|
233
|
+
return "resetting"
|
|
234
|
+
case .unsupported:
|
|
235
|
+
return "unsupported"
|
|
236
|
+
case .unauthorized:
|
|
237
|
+
return "unauthorized"
|
|
238
|
+
case .poweredOff:
|
|
239
|
+
return "powered off"
|
|
240
|
+
case .poweredOn:
|
|
241
|
+
return "powered on"
|
|
242
|
+
@unknown default:
|
|
243
|
+
return "unknown state"
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@blunt-ly/ble-advertiser",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Minimal BLE advertiser for React-Native, optimized for background processing.",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"types": "build/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "node internal/module_scripts/build.js",
|
|
9
|
+
"clean": "node internal/module_scripts/clean.js",
|
|
10
|
+
"lint": "eslint src/",
|
|
11
|
+
"prepare": "node internal/module_scripts/prepare.js",
|
|
12
|
+
"open:ios": "node internal/module_scripts/open-ios.js",
|
|
13
|
+
"open:android": "node internal/module_scripts/open-android.js"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"react-native",
|
|
17
|
+
"expo",
|
|
18
|
+
"bluetooth",
|
|
19
|
+
"ble",
|
|
20
|
+
"advertiser",
|
|
21
|
+
"ble-advertiser"
|
|
22
|
+
],
|
|
23
|
+
"repository": "https://github.com/Blunt-ly/ble-advertiser",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/Blunt-ly/ble-advertiser/issues"
|
|
26
|
+
},
|
|
27
|
+
"author": "The Blunt.ly Authors",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"homepage": "https://github.com/Blunt-ly/ble-advertiser#readme",
|
|
30
|
+
"files": [
|
|
31
|
+
"build",
|
|
32
|
+
"src",
|
|
33
|
+
"android",
|
|
34
|
+
"ios",
|
|
35
|
+
"expo-module.config.json",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"dependencies": {},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/react": "~19.1.1",
|
|
42
|
+
"eslint": "~9.39.4",
|
|
43
|
+
"eslint-config-universe": "^15.0.3",
|
|
44
|
+
"expo": "^56.0.3",
|
|
45
|
+
"react-native": "0.82.1",
|
|
46
|
+
"typescript": "^5.9.2"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"expo": "*",
|
|
50
|
+
"react": "*",
|
|
51
|
+
"react-native": "*"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type AdvertisingStartedEvent = {
|
|
2
|
+
uuids: string[];
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export type AdvertisingFailedEvent = {
|
|
6
|
+
error: string;
|
|
7
|
+
errorCode?: number;
|
|
8
|
+
uuids?: string[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type AdvertisingStoppedEvent = {
|
|
12
|
+
uuids?: string[];
|
|
13
|
+
stoppedCount?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type BleAdvertiserModuleEvents = {
|
|
17
|
+
onAdvertisingStarted: (params: AdvertisingStartedEvent) => void;
|
|
18
|
+
onAdvertisingFailed: (params: AdvertisingFailedEvent) => void;
|
|
19
|
+
onAdvertisingStopped: (params: AdvertisingStoppedEvent) => void;
|
|
20
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { NativeModule, requireNativeModule } from 'expo';
|
|
2
|
+
|
|
3
|
+
import { BleAdvertiserModuleEvents } from './BleAdvertiser.types';
|
|
4
|
+
|
|
5
|
+
declare class BleAdvertiserModule extends NativeModule<BleAdvertiserModuleEvents> {
|
|
6
|
+
/**
|
|
7
|
+
* Start BLE advertising with the specified service UUIDs
|
|
8
|
+
* @param uuids Array of service UUID strings to advertise
|
|
9
|
+
* @returns Promise that resolves when advertising starts successfully
|
|
10
|
+
*/
|
|
11
|
+
broadcast(uuids: string[]): Promise<string>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Stop all BLE advertising
|
|
15
|
+
* @returns Promise that resolves with information about stopped advertisers
|
|
16
|
+
*/
|
|
17
|
+
stopBroadcast(): Promise<{ stopped: boolean; stoppedAdvertisers?: number }>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if BLE advertising is supported on this device
|
|
21
|
+
* @returns Promise that resolves to true if supported, false otherwise
|
|
22
|
+
*/
|
|
23
|
+
isSupported(): Promise<boolean>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if Bluetooth is currently enabled
|
|
27
|
+
* @returns Promise that resolves to true if enabled, false otherwise
|
|
28
|
+
*/
|
|
29
|
+
isEnabled(): Promise<boolean>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// This call loads the native module object from the JSI.
|
|
33
|
+
export default requireNativeModule<BleAdvertiserModule>('BleAdvertiser');
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { EventSubscription } from 'expo-modules-core';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
AdvertisingStartedEvent,
|
|
6
|
+
AdvertisingFailedEvent,
|
|
7
|
+
AdvertisingStoppedEvent,
|
|
8
|
+
} from './BleAdvertiser.types';
|
|
9
|
+
import BleAdvertiserModule from './BleAdvertiserModule';
|
|
10
|
+
import { expand16BitUuid } from './utils';
|
|
11
|
+
|
|
12
|
+
// HACK: Android's BluetoothLeAdvertiser only accepts 128-bit service UUIDs, while
|
|
13
|
+
// CoreBluetooth on iOS accepts the 16-bit short form directly. Expand any
|
|
14
|
+
// 16-bit input before handing it to the Android native module.
|
|
15
|
+
function normalizeUuidsForPlatform(uuids: string[]): string[] {
|
|
16
|
+
if (Platform.OS !== 'android') return uuids;
|
|
17
|
+
return uuids.map((uuid) => {
|
|
18
|
+
const hex = uuid.replace(/[^0-9a-fA-F]/g, '');
|
|
19
|
+
return hex.length === 4 ? expand16BitUuid(uuid) : uuid;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Start BLE advertising with the specified service UUIDs
|
|
25
|
+
* @param uuids Array of service UUID strings to advertise
|
|
26
|
+
* @returns Promise that resolves when advertising starts successfully
|
|
27
|
+
*/
|
|
28
|
+
export function broadcast(uuids: string[]): Promise<string> {
|
|
29
|
+
return BleAdvertiserModule.broadcast(normalizeUuidsForPlatform(uuids));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Stop all BLE advertising
|
|
34
|
+
* @returns Promise that resolves with information about stopped advertisers
|
|
35
|
+
*/
|
|
36
|
+
export function stopBroadcast(): Promise<{ stopped: boolean; stoppedAdvertisers?: number }> {
|
|
37
|
+
return BleAdvertiserModule.stopBroadcast();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if BLE advertising is supported on this device
|
|
42
|
+
* @returns Promise that resolves to true if supported, false otherwise
|
|
43
|
+
*/
|
|
44
|
+
export function isSupported(): Promise<boolean> {
|
|
45
|
+
return BleAdvertiserModule.isSupported();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if Bluetooth is currently enabled
|
|
50
|
+
* @returns Promise that resolves to true if enabled, false otherwise
|
|
51
|
+
*/
|
|
52
|
+
export function isEnabled(): Promise<boolean> {
|
|
53
|
+
return BleAdvertiserModule.isEnabled();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Add listener for advertising started events
|
|
58
|
+
* @param listener Function to call when advertising starts
|
|
59
|
+
* @returns EventSubscription that can be used to remove the listener
|
|
60
|
+
*/
|
|
61
|
+
export function addAdvertisingStartedListener(
|
|
62
|
+
listener: (event: AdvertisingStartedEvent) => void
|
|
63
|
+
): EventSubscription {
|
|
64
|
+
return BleAdvertiserModule.addListener('onAdvertisingStarted', listener);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Add listener for advertising failed events
|
|
69
|
+
* @param listener Function to call when advertising fails
|
|
70
|
+
* @returns EventSubscription that can be used to remove the listener
|
|
71
|
+
*/
|
|
72
|
+
export function addAdvertisingFailedListener(
|
|
73
|
+
listener: (event: AdvertisingFailedEvent) => void
|
|
74
|
+
): EventSubscription {
|
|
75
|
+
return BleAdvertiserModule.addListener('onAdvertisingFailed', listener);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Add listener for advertising stopped events
|
|
80
|
+
* @param listener Function to call when advertising stops
|
|
81
|
+
* @returns EventSubscription that can be used to remove the listener
|
|
82
|
+
*/
|
|
83
|
+
export function addAdvertisingStoppedListener(
|
|
84
|
+
listener: (event: AdvertisingStoppedEvent) => void
|
|
85
|
+
): EventSubscription {
|
|
86
|
+
return BleAdvertiserModule.addListener('onAdvertisingStopped', listener);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Export the types for consumers
|
|
90
|
+
export * from './BleAdvertiser.types';
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expands a 16-bit UUID to the full 128-bit Bluetooth UUID format.
|
|
3
|
+
*
|
|
4
|
+
* @param shortUuid 16-bit UUID string (with or without dashes)
|
|
5
|
+
* @returns Expanded 128-bit UUID string
|
|
6
|
+
* @throws Error if the input is not a valid 16-bit UUID
|
|
7
|
+
*/
|
|
8
|
+
export function expand16BitUuid(shortUuid: string): string {
|
|
9
|
+
// Clean and validate input
|
|
10
|
+
const cleaned = shortUuid.replace(/[^0-9a-fA-F]/g, '');
|
|
11
|
+
|
|
12
|
+
if (!/^[0-9a-fA-F]{4}$/i.test(cleaned)) {
|
|
13
|
+
throw new Error(`Invalid 16-bit UUID: ${shortUuid}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Pad to 4 chars and convert to lowercase
|
|
17
|
+
const paddedUuid = cleaned.padStart(4, '0').toLowerCase();
|
|
18
|
+
|
|
19
|
+
// Bluetooth Base UUID: 00000000-0000-1000-8000-00805F9B34FB
|
|
20
|
+
return `0000${paddedUuid}-0000-1000-8000-00805f9b34fb`;
|
|
21
|
+
}
|