munim-bluetooth 0.3.19 → 0.3.21
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.
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
package com.munimbluetooth
|
|
2
2
|
|
|
3
|
-
import android.bluetooth
|
|
4
|
-
import android.bluetooth.
|
|
3
|
+
import android.bluetooth.BluetoothAdapter
|
|
4
|
+
import android.bluetooth.BluetoothDevice
|
|
5
|
+
import android.bluetooth.BluetoothGatt
|
|
6
|
+
import android.bluetooth.BluetoothGattCallback
|
|
7
|
+
import android.bluetooth.BluetoothGattCharacteristic
|
|
8
|
+
import android.bluetooth.BluetoothGattDescriptor
|
|
9
|
+
import android.bluetooth.BluetoothGattServer
|
|
10
|
+
import android.bluetooth.BluetoothGattServerCallback
|
|
11
|
+
import android.bluetooth.BluetoothGattService
|
|
12
|
+
import android.bluetooth.BluetoothManager
|
|
13
|
+
import android.bluetooth.BluetoothProfile
|
|
14
|
+
import android.bluetooth.le.AdvertiseCallback
|
|
15
|
+
import android.bluetooth.le.AdvertiseData
|
|
16
|
+
import android.bluetooth.le.AdvertiseSettings
|
|
17
|
+
import android.bluetooth.le.BluetoothLeAdvertiser
|
|
18
|
+
import android.bluetooth.le.BluetoothLeScanner
|
|
19
|
+
import android.bluetooth.le.ScanCallback
|
|
20
|
+
import android.bluetooth.le.ScanFilter
|
|
21
|
+
import android.bluetooth.le.ScanRecord
|
|
22
|
+
import android.bluetooth.le.ScanResult
|
|
23
|
+
import android.bluetooth.le.ScanSettings
|
|
5
24
|
import android.content.Context
|
|
6
25
|
import android.os.Build
|
|
7
26
|
import android.os.ParcelUuid
|
|
@@ -11,682 +30,773 @@ import com.facebook.react.bridge.WritableArray
|
|
|
11
30
|
import com.facebook.react.bridge.WritableMap
|
|
12
31
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
13
32
|
import com.margelo.nitro.NitroModules
|
|
33
|
+
import com.margelo.nitro.core.Promise
|
|
34
|
+
import com.margelo.nitro.munimbluetooth.AdvertisingDataTypes
|
|
35
|
+
import com.margelo.nitro.munimbluetooth.AdvertisingOptions
|
|
36
|
+
import com.margelo.nitro.munimbluetooth.CharacteristicValue
|
|
37
|
+
import com.margelo.nitro.munimbluetooth.GATTCharacteristic
|
|
38
|
+
import com.margelo.nitro.munimbluetooth.GATTService
|
|
14
39
|
import com.margelo.nitro.munimbluetooth.HybridMunimBluetoothSpec
|
|
15
|
-
import
|
|
16
|
-
import
|
|
40
|
+
import com.margelo.nitro.munimbluetooth.ScanMode
|
|
41
|
+
import com.margelo.nitro.munimbluetooth.ScanOptions
|
|
42
|
+
import com.margelo.nitro.munimbluetooth.ServiceDataEntry
|
|
43
|
+
import com.margelo.nitro.munimbluetooth.WriteType
|
|
44
|
+
import kotlinx.coroutines.CoroutineScope
|
|
45
|
+
import kotlinx.coroutines.Dispatchers
|
|
46
|
+
import kotlinx.coroutines.Job
|
|
47
|
+
import kotlinx.coroutines.SupervisorJob
|
|
48
|
+
import kotlinx.coroutines.delay
|
|
49
|
+
import kotlinx.coroutines.launch
|
|
50
|
+
import java.util.UUID
|
|
17
51
|
|
|
18
52
|
class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
19
|
-
private val
|
|
20
|
-
|
|
21
|
-
// Peripheral Manager
|
|
53
|
+
private val bluetoothScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
|
54
|
+
|
|
22
55
|
private var advertiser: BluetoothLeAdvertiser? = null
|
|
56
|
+
private var advertiseCallback: AdvertiseCallback? = null
|
|
23
57
|
private var gattServer: BluetoothGattServer? = null
|
|
24
58
|
private var gattServerReady = false
|
|
25
59
|
private var advertiseJob: Job? = null
|
|
26
|
-
private var currentAdvertisingData:
|
|
60
|
+
private var currentAdvertisingData: AdvertisingDataTypes? = null
|
|
61
|
+
private var currentServiceUUIDs: Array<String> = emptyArray()
|
|
62
|
+
private var currentLocalName: String? = null
|
|
63
|
+
private var currentManufacturerData: String? = null
|
|
27
64
|
private var bluetoothManager: BluetoothManager? = null
|
|
28
65
|
private var bluetoothAdapter: BluetoothAdapter? = null
|
|
29
|
-
|
|
30
|
-
// Central Manager
|
|
66
|
+
|
|
31
67
|
private var bluetoothLeScanner: BluetoothLeScanner? = null
|
|
32
68
|
private var scanCallback: ScanCallback? = null
|
|
33
69
|
private var isScanning = false
|
|
34
70
|
private val discoveredDevices = mutableMapOf<String, BluetoothDevice>()
|
|
35
71
|
private val connectedDevices = mutableMapOf<String, BluetoothGatt>()
|
|
36
|
-
private val
|
|
72
|
+
private val pendingConnections = mutableMapOf<String, Promise<Unit>>()
|
|
73
|
+
private val pendingServiceDiscoveries = mutableMapOf<String, Promise<Array<GATTService>>>()
|
|
74
|
+
private val pendingReads = mutableMapOf<String, Promise<CharacteristicValue>>()
|
|
75
|
+
private val pendingWrites = mutableMapOf<String, Promise<Unit>>()
|
|
76
|
+
private val pendingRssiReads = mutableMapOf<String, Promise<Double>>()
|
|
77
|
+
private val lastCharacteristicValues = mutableMapOf<String, CharacteristicValue>()
|
|
78
|
+
private val lastRssiValues = mutableMapOf<String, Double>()
|
|
37
79
|
private val eventEmitter = NitroEventEmitter(TAG)
|
|
38
|
-
|
|
39
|
-
init {
|
|
40
|
-
// Initialize Bluetooth managers - this would need ReactApplicationContext in real implementation
|
|
41
|
-
// For now, we'll initialize them when needed
|
|
42
|
-
}
|
|
43
|
-
|
|
80
|
+
|
|
44
81
|
private fun getBluetoothManager(): BluetoothManager? {
|
|
45
82
|
val context = NitroModules.applicationContext ?: return null
|
|
46
83
|
return context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
|
|
47
84
|
}
|
|
48
|
-
|
|
85
|
+
|
|
49
86
|
private fun ensureBluetoothManager() {
|
|
50
87
|
if (bluetoothManager == null) {
|
|
51
88
|
bluetoothManager = getBluetoothManager()
|
|
52
89
|
bluetoothAdapter = bluetoothManager?.adapter
|
|
53
90
|
}
|
|
54
91
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
override fun startAdvertising(options: Map<String, Any>) {
|
|
92
|
+
|
|
93
|
+
override fun startAdvertising(options: AdvertisingOptions) {
|
|
59
94
|
ensureBluetoothManager()
|
|
60
95
|
val adapter = bluetoothAdapter
|
|
61
96
|
if (adapter == null || !adapter.isEnabled) {
|
|
62
97
|
Log.e(TAG, "Bluetooth is not enabled or not available")
|
|
63
98
|
return
|
|
64
99
|
}
|
|
65
|
-
|
|
66
|
-
val serviceUUIDs = options["serviceUUIDs"] as? List<String>
|
|
67
|
-
if (serviceUUIDs == null || serviceUUIDs.isEmpty()) {
|
|
100
|
+
if (options.serviceUUIDs.isEmpty()) {
|
|
68
101
|
Log.e(TAG, "No service UUIDs provided for advertising")
|
|
69
102
|
return
|
|
70
103
|
}
|
|
71
|
-
|
|
72
|
-
|
|
104
|
+
|
|
105
|
+
currentServiceUUIDs = options.serviceUUIDs
|
|
106
|
+
currentLocalName = options.localName
|
|
107
|
+
currentManufacturerData = options.manufacturerData
|
|
108
|
+
currentAdvertisingData = normalizeAdvertisingData(
|
|
109
|
+
options.advertisingData,
|
|
110
|
+
options.localName,
|
|
111
|
+
options.manufacturerData
|
|
112
|
+
)
|
|
113
|
+
|
|
73
114
|
if (!gattServerReady) {
|
|
74
|
-
setServicesFromOptions(serviceUUIDs)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Cancel any previous advertising job
|
|
78
|
-
advertiseJob?.cancel()
|
|
79
|
-
advertiseJob = CoroutineScope(Dispatchers.Main).launch {
|
|
80
|
-
delay(300) // Wait for GATT server to be ready
|
|
81
|
-
advertiser = adapter.bluetoothLeAdvertiser
|
|
82
|
-
|
|
83
|
-
val settings = AdvertiseSettings.Builder()
|
|
84
|
-
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
|
|
85
|
-
.setConnectable(true)
|
|
86
|
-
.setTimeout(0)
|
|
87
|
-
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
|
|
88
|
-
.build()
|
|
89
|
-
|
|
90
|
-
val dataBuilder = AdvertiseData.Builder()
|
|
91
|
-
|
|
92
|
-
// Process comprehensive advertising data
|
|
93
|
-
val advertisingDataMap = options["advertisingData"] as? Map<String, Any>
|
|
94
|
-
if (advertisingDataMap != null) {
|
|
95
|
-
processAdvertisingData(advertisingDataMap, dataBuilder)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Legacy support - add service UUIDs
|
|
99
|
-
for (uuid in serviceUUIDs) {
|
|
100
|
-
dataBuilder.addServiceUuid(ParcelUuid.fromString(uuid))
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Legacy support - local name
|
|
104
|
-
val localName = options["localName"] as? String
|
|
105
|
-
if (localName != null) {
|
|
106
|
-
dataBuilder.setIncludeDeviceName(true)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Legacy support - manufacturer data
|
|
110
|
-
val manufacturerData = options["manufacturerData"] as? String
|
|
111
|
-
if (manufacturerData != null) {
|
|
112
|
-
val data = hexStringToByteArray(manufacturerData)
|
|
113
|
-
if (data != null) {
|
|
114
|
-
dataBuilder.addManufacturerData(0x0000, data) // Default manufacturer code
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
currentAdvertisingData = mapOf(
|
|
119
|
-
"advertisingData" to (advertisingDataMap ?: emptyMap<String, Any>()),
|
|
120
|
-
"localName" to (localName ?: ""),
|
|
121
|
-
"manufacturerData" to (manufacturerData ?: "")
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
advertiser?.startAdvertising(settings, dataBuilder.build(), object : AdvertiseCallback() {
|
|
125
|
-
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
|
|
126
|
-
Log.i(TAG, "Advertising started successfully")
|
|
127
|
-
}
|
|
128
|
-
override fun onStartFailure(errorCode: Int) {
|
|
129
|
-
Log.e(TAG, "Advertising failed: $errorCode")
|
|
130
|
-
}
|
|
131
|
-
})
|
|
115
|
+
setServicesFromOptions(options.serviceUUIDs)
|
|
132
116
|
}
|
|
117
|
+
restartAdvertising(delayMs = 300L)
|
|
133
118
|
}
|
|
134
|
-
|
|
135
|
-
override fun updateAdvertisingData(advertisingData:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
advertiser?.stopAdvertising(object : AdvertiseCallback() {})
|
|
144
|
-
|
|
145
|
-
advertiseJob?.cancel()
|
|
146
|
-
advertiseJob = CoroutineScope(Dispatchers.Main).launch {
|
|
147
|
-
delay(100) // Brief delay before restarting
|
|
148
|
-
advertiser = adapter.bluetoothLeAdvertiser
|
|
149
|
-
|
|
150
|
-
val settings = AdvertiseSettings.Builder()
|
|
151
|
-
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
|
|
152
|
-
.setConnectable(true)
|
|
153
|
-
.setTimeout(0)
|
|
154
|
-
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
|
|
155
|
-
.build()
|
|
156
|
-
|
|
157
|
-
val dataBuilder = AdvertiseData.Builder()
|
|
158
|
-
processAdvertisingData(advertisingData, dataBuilder)
|
|
159
|
-
|
|
160
|
-
currentAdvertisingData = mapOf("advertisingData" to advertisingData)
|
|
161
|
-
|
|
162
|
-
advertiser?.startAdvertising(settings, dataBuilder.build(), object : AdvertiseCallback() {
|
|
163
|
-
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
|
|
164
|
-
Log.i(TAG, "Advertising updated successfully")
|
|
165
|
-
}
|
|
166
|
-
override fun onStartFailure(errorCode: Int) {
|
|
167
|
-
Log.e(TAG, "Advertising update failed: $errorCode")
|
|
168
|
-
}
|
|
169
|
-
})
|
|
119
|
+
|
|
120
|
+
override fun updateAdvertisingData(advertisingData: AdvertisingDataTypes) {
|
|
121
|
+
currentAdvertisingData = normalizeAdvertisingData(
|
|
122
|
+
advertisingData,
|
|
123
|
+
currentLocalName,
|
|
124
|
+
currentManufacturerData
|
|
125
|
+
)
|
|
126
|
+
if (currentServiceUUIDs.isNotEmpty()) {
|
|
127
|
+
restartAdvertising(delayMs = 100L)
|
|
170
128
|
}
|
|
171
129
|
}
|
|
172
|
-
|
|
173
|
-
override fun getAdvertisingData():
|
|
174
|
-
return currentAdvertisingData ?:
|
|
130
|
+
|
|
131
|
+
override fun getAdvertisingData(): Promise<AdvertisingDataTypes> {
|
|
132
|
+
return Promise.resolved(currentAdvertisingData ?: emptyAdvertisingData())
|
|
175
133
|
}
|
|
176
|
-
|
|
134
|
+
|
|
177
135
|
override fun stopAdvertising() {
|
|
178
|
-
advertiser?.stopAdvertising(object : AdvertiseCallback() {})
|
|
179
|
-
advertiser = null
|
|
180
136
|
advertiseJob?.cancel()
|
|
137
|
+
advertiseCallback?.let { callback ->
|
|
138
|
+
advertiser?.stopAdvertising(callback)
|
|
139
|
+
}
|
|
140
|
+
advertiseCallback = null
|
|
141
|
+
advertiser = null
|
|
181
142
|
currentAdvertisingData = null
|
|
143
|
+
currentServiceUUIDs = emptyArray()
|
|
144
|
+
currentLocalName = null
|
|
145
|
+
currentManufacturerData = null
|
|
182
146
|
}
|
|
183
|
-
|
|
184
|
-
override fun setServices(services:
|
|
147
|
+
|
|
148
|
+
override fun setServices(services: Array<GATTService>) {
|
|
185
149
|
ensureBluetoothManager()
|
|
186
150
|
gattServerReady = false
|
|
187
|
-
|
|
151
|
+
|
|
188
152
|
val manager = bluetoothManager ?: return
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
override fun onCharacteristicReadRequest(
|
|
195
|
-
device: BluetoothDevice,
|
|
196
|
-
requestId: Int,
|
|
197
|
-
offset: Int,
|
|
198
|
-
characteristic: BluetoothGattCharacteristic
|
|
199
|
-
) {
|
|
200
|
-
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, characteristic.value)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
override fun onCharacteristicWriteRequest(
|
|
204
|
-
device: BluetoothDevice,
|
|
205
|
-
requestId: Int,
|
|
206
|
-
characteristic: BluetoothGattCharacteristic,
|
|
207
|
-
preparedWrite: Boolean,
|
|
208
|
-
responseNeeded: Boolean,
|
|
209
|
-
offset: Int,
|
|
210
|
-
value: ByteArray?
|
|
211
|
-
) {
|
|
212
|
-
if (responseNeeded) {
|
|
213
|
-
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null)
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
})
|
|
217
|
-
|
|
153
|
+
val context = NitroModules.applicationContext ?: return
|
|
154
|
+
|
|
155
|
+
gattServer?.close()
|
|
156
|
+
gattServer = manager.openGattServer(context, buildGattServerCallback())
|
|
218
157
|
gattServer?.clearServices()
|
|
219
|
-
|
|
220
|
-
for (
|
|
221
|
-
val serviceUuid = serviceMap["uuid"] as? String ?: continue
|
|
158
|
+
|
|
159
|
+
for (serviceData in services) {
|
|
222
160
|
val service = BluetoothGattService(
|
|
223
|
-
UUID.fromString(
|
|
161
|
+
UUID.fromString(serviceData.uuid),
|
|
224
162
|
BluetoothGattService.SERVICE_TYPE_PRIMARY
|
|
225
163
|
)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
when (prop) {
|
|
237
|
-
"read" -> properties = properties or BluetoothGattCharacteristic.PROPERTY_READ
|
|
238
|
-
"write" -> properties = properties or BluetoothGattCharacteristic.PROPERTY_WRITE
|
|
239
|
-
"notify" -> properties = properties or BluetoothGattCharacteristic.PROPERTY_NOTIFY
|
|
240
|
-
"indicate" -> properties = properties or BluetoothGattCharacteristic.PROPERTY_INDICATE
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
val permissions = BluetoothGattCharacteristic.PERMISSION_READ or
|
|
246
|
-
BluetoothGattCharacteristic.PERMISSION_WRITE
|
|
247
|
-
|
|
248
|
-
val characteristic = BluetoothGattCharacteristic(
|
|
249
|
-
UUID.fromString(charUuid),
|
|
250
|
-
properties,
|
|
251
|
-
permissions
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
val value = charMap["value"] as? String
|
|
255
|
-
if (value != null) {
|
|
256
|
-
characteristic.value = value.toByteArray()
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
service.addCharacteristic(characteristic)
|
|
164
|
+
|
|
165
|
+
for (characteristicData in serviceData.characteristics) {
|
|
166
|
+
val characteristic = BluetoothGattCharacteristic(
|
|
167
|
+
UUID.fromString(characteristicData.uuid),
|
|
168
|
+
propertiesFromArray(characteristicData.properties),
|
|
169
|
+
BluetoothGattCharacteristic.PERMISSION_READ or
|
|
170
|
+
BluetoothGattCharacteristic.PERMISSION_WRITE
|
|
171
|
+
)
|
|
172
|
+
characteristicData.value?.let { value ->
|
|
173
|
+
characteristic.value = hexStringToByteArray(value) ?: value.toByteArray()
|
|
260
174
|
}
|
|
175
|
+
service.addCharacteristic(characteristic)
|
|
261
176
|
}
|
|
262
|
-
|
|
177
|
+
|
|
263
178
|
gattServer?.addService(service)
|
|
264
179
|
}
|
|
265
|
-
|
|
180
|
+
|
|
266
181
|
gattServerReady = true
|
|
267
182
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
override fun isBluetoothEnabled(): Boolean {
|
|
183
|
+
|
|
184
|
+
override fun isBluetoothEnabled(): Promise<Boolean> {
|
|
272
185
|
ensureBluetoothManager()
|
|
273
|
-
return bluetoothAdapter?.isEnabled == true
|
|
186
|
+
return Promise.resolved(bluetoothAdapter?.isEnabled == true)
|
|
274
187
|
}
|
|
275
|
-
|
|
276
|
-
override fun requestBluetoothPermission(): Boolean {
|
|
277
|
-
|
|
278
|
-
// This would need Activity context in real implementation
|
|
279
|
-
return true
|
|
188
|
+
|
|
189
|
+
override fun requestBluetoothPermission(): Promise<Boolean> {
|
|
190
|
+
return Promise.resolved(true)
|
|
280
191
|
}
|
|
281
|
-
|
|
282
|
-
override fun startScan(options:
|
|
192
|
+
|
|
193
|
+
override fun startScan(options: ScanOptions?) {
|
|
283
194
|
ensureBluetoothManager()
|
|
284
195
|
val adapter = bluetoothAdapter
|
|
285
196
|
if (adapter == null || !adapter.isEnabled) {
|
|
286
197
|
Log.e(TAG, "Bluetooth is not enabled or not available")
|
|
287
198
|
return
|
|
288
199
|
}
|
|
289
|
-
|
|
290
200
|
if (isScanning) return
|
|
291
|
-
|
|
201
|
+
|
|
202
|
+
val scanner = adapter.bluetoothLeScanner
|
|
203
|
+
if (scanner == null) {
|
|
204
|
+
Log.e(TAG, "Bluetooth LE scanner is not available")
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
292
208
|
isScanning = true
|
|
293
209
|
discoveredDevices.clear()
|
|
294
|
-
|
|
295
|
-
val scanner = adapter.bluetoothLeScanner
|
|
296
210
|
bluetoothLeScanner = scanner
|
|
297
|
-
|
|
298
|
-
val
|
|
299
|
-
|
|
300
|
-
|
|
211
|
+
|
|
212
|
+
val scanFilters = options?.serviceUUIDs
|
|
213
|
+
?.takeIf { it.isNotEmpty() }
|
|
214
|
+
?.map { uuid ->
|
|
301
215
|
ScanFilter.Builder()
|
|
302
216
|
.setServiceUuid(ParcelUuid.fromString(uuid))
|
|
303
217
|
.build()
|
|
304
218
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
"lowPower" -> ScanSettings.SCAN_MODE_LOW_POWER
|
|
311
|
-
"balanced" -> ScanSettings.SCAN_MODE_BALANCED
|
|
312
|
-
"lowLatency" -> ScanSettings.SCAN_MODE_LOW_LATENCY
|
|
219
|
+
?: emptyList()
|
|
220
|
+
|
|
221
|
+
val scanMode = when (options?.scanMode) {
|
|
222
|
+
ScanMode.LOWPOWER -> ScanSettings.SCAN_MODE_LOW_POWER
|
|
223
|
+
ScanMode.LOWLATENCY -> ScanSettings.SCAN_MODE_LOW_LATENCY
|
|
313
224
|
else -> ScanSettings.SCAN_MODE_BALANCED
|
|
314
225
|
}
|
|
315
|
-
|
|
226
|
+
|
|
316
227
|
val scanSettings = ScanSettings.Builder()
|
|
317
228
|
.setScanMode(scanMode)
|
|
318
229
|
.build()
|
|
319
|
-
|
|
230
|
+
|
|
320
231
|
scanCallback = object : ScanCallback() {
|
|
321
232
|
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
|
322
233
|
val device = result.device
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
eventEmitter.emit(
|
|
327
|
-
"deviceFound",
|
|
328
|
-
buildScanPayload(result)
|
|
329
|
-
)
|
|
234
|
+
discoveredDevices[device.address] = device
|
|
235
|
+
eventEmitter.emit("deviceFound", buildScanPayload(result))
|
|
330
236
|
}
|
|
331
|
-
|
|
237
|
+
|
|
332
238
|
override fun onBatchScanResults(results: MutableList<ScanResult>) {
|
|
333
|
-
|
|
334
|
-
|
|
239
|
+
results.forEach { result ->
|
|
240
|
+
val device = result.device
|
|
241
|
+
discoveredDevices[device.address] = device
|
|
242
|
+
eventEmitter.emit("deviceFound", buildScanPayload(result))
|
|
335
243
|
}
|
|
336
244
|
}
|
|
337
|
-
|
|
245
|
+
|
|
338
246
|
override fun onScanFailed(errorCode: Int) {
|
|
339
247
|
Log.e(TAG, "Scan failed: $errorCode")
|
|
340
248
|
isScanning = false
|
|
341
249
|
}
|
|
342
250
|
}
|
|
343
|
-
|
|
251
|
+
|
|
344
252
|
scanner.startScan(scanFilters, scanSettings, scanCallback)
|
|
345
253
|
}
|
|
346
|
-
|
|
254
|
+
|
|
347
255
|
override fun stopScan() {
|
|
348
256
|
if (!isScanning) return
|
|
349
|
-
|
|
350
|
-
|
|
257
|
+
scanCallback?.let { callback ->
|
|
258
|
+
bluetoothLeScanner?.stopScan(callback)
|
|
259
|
+
}
|
|
351
260
|
bluetoothLeScanner = null
|
|
352
261
|
scanCallback = null
|
|
353
262
|
isScanning = false
|
|
354
263
|
}
|
|
355
|
-
|
|
356
|
-
override fun connect(deviceId: String) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
val gatt = device.connectGatt(null, false, object : BluetoothGattCallback() {
|
|
364
|
-
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
|
365
|
-
if (newState == BluetoothProfile.STATE_CONNECTED) {
|
|
366
|
-
connectedDevices[deviceId] = gatt
|
|
367
|
-
gatt.discoverServices()
|
|
368
|
-
// Emit deviceConnected event
|
|
369
|
-
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
|
|
370
|
-
connectedDevices.remove(deviceId)
|
|
371
|
-
deviceCharacteristics.remove(deviceId)
|
|
372
|
-
// Emit deviceDisconnected event
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
|
377
|
-
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
378
|
-
val characteristics = mutableListOf<BluetoothGattCharacteristic>()
|
|
379
|
-
for (service in gatt.services) {
|
|
380
|
-
characteristics.addAll(service.characteristics)
|
|
381
|
-
}
|
|
382
|
-
deviceCharacteristics[deviceId] = characteristics
|
|
383
|
-
// Emit servicesDiscovered event
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
override fun onCharacteristicRead(
|
|
388
|
-
gatt: BluetoothGatt,
|
|
389
|
-
characteristic: BluetoothGattCharacteristic,
|
|
390
|
-
status: Int
|
|
391
|
-
) {
|
|
392
|
-
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
393
|
-
val hexValue = characteristic.value?.joinToString("") { "%02x".format(it) } ?: ""
|
|
394
|
-
// Emit characteristicValueChanged event
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
override fun onCharacteristicWrite(
|
|
399
|
-
gatt: BluetoothGatt,
|
|
400
|
-
characteristic: BluetoothGattCharacteristic,
|
|
401
|
-
status: Int
|
|
402
|
-
) {
|
|
403
|
-
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
404
|
-
// Emit writeSuccess event
|
|
405
|
-
} else {
|
|
406
|
-
// Emit writeError event
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
override fun onCharacteristicChanged(
|
|
411
|
-
gatt: BluetoothGatt,
|
|
412
|
-
characteristic: BluetoothGattCharacteristic
|
|
413
|
-
) {
|
|
414
|
-
val hexValue = characteristic.value?.joinToString("") { "%02x".format(it) } ?: ""
|
|
415
|
-
// Emit characteristicValueChanged event
|
|
264
|
+
|
|
265
|
+
override fun connect(deviceId: String): Promise<Unit> {
|
|
266
|
+
ensureBluetoothManager()
|
|
267
|
+
connectedDevices[deviceId]?.let { existingGatt ->
|
|
268
|
+
if (existingGatt.services != null) {
|
|
269
|
+
return Promise.resolved(Unit)
|
|
416
270
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
val context = NitroModules.applicationContext
|
|
274
|
+
?: return Promise.rejected(IllegalStateException("React context unavailable"))
|
|
275
|
+
val adapter = bluetoothAdapter
|
|
276
|
+
?: return Promise.rejected(IllegalStateException("Bluetooth adapter unavailable"))
|
|
277
|
+
|
|
278
|
+
val device = discoveredDevices[deviceId] ?: run {
|
|
279
|
+
try {
|
|
280
|
+
adapter.getRemoteDevice(deviceId)
|
|
281
|
+
} catch (_: IllegalArgumentException) {
|
|
282
|
+
null
|
|
422
283
|
}
|
|
423
|
-
})
|
|
284
|
+
} ?: return Promise.rejected(IllegalArgumentException("Device not found: $deviceId"))
|
|
285
|
+
|
|
286
|
+
val promise = Promise<Unit>()
|
|
287
|
+
pendingConnections[deviceId] = promise
|
|
288
|
+
|
|
289
|
+
val gatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
290
|
+
device.connectGatt(context, false, createGattCallback(deviceId), BluetoothDevice.TRANSPORT_LE)
|
|
291
|
+
} else {
|
|
292
|
+
device.connectGatt(context, false, createGattCallback(deviceId))
|
|
293
|
+
}
|
|
294
|
+
connectedDevices[deviceId] = gatt
|
|
295
|
+
return promise
|
|
424
296
|
}
|
|
425
|
-
|
|
297
|
+
|
|
426
298
|
override fun disconnect(deviceId: String) {
|
|
427
|
-
|
|
299
|
+
pendingConnections.remove(deviceId)
|
|
300
|
+
pendingServiceDiscoveries.remove(deviceId)
|
|
301
|
+
pendingRssiReads.remove(deviceId)
|
|
302
|
+
|
|
303
|
+
val gatt = connectedDevices.remove(deviceId)
|
|
428
304
|
gatt?.disconnect()
|
|
429
305
|
gatt?.close()
|
|
430
|
-
|
|
431
|
-
|
|
306
|
+
|
|
307
|
+
rejectPendingOperationsForDevice(deviceId, IllegalStateException("Disconnected from $deviceId"))
|
|
308
|
+
eventEmitter.emit("deviceDisconnected", mapOf("deviceId" to deviceId))
|
|
432
309
|
}
|
|
433
|
-
|
|
434
|
-
override fun discoverServices(deviceId: String):
|
|
310
|
+
|
|
311
|
+
override fun discoverServices(deviceId: String): Promise<Array<GATTService>> {
|
|
435
312
|
val gatt = connectedDevices[deviceId]
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
313
|
+
?: return Promise.rejected(IllegalStateException("Device not connected: $deviceId"))
|
|
314
|
+
|
|
315
|
+
if (gatt.services.isNotEmpty()) {
|
|
316
|
+
return Promise.resolved(buildGattServices(gatt))
|
|
439
317
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
318
|
+
|
|
319
|
+
val promise = Promise<Array<GATTService>>()
|
|
320
|
+
pendingServiceDiscoveries[deviceId] = promise
|
|
321
|
+
if (!gatt.discoverServices()) {
|
|
322
|
+
pendingServiceDiscoveries.remove(deviceId)
|
|
323
|
+
return Promise.rejected(IllegalStateException("Failed to start service discovery for $deviceId"))
|
|
324
|
+
}
|
|
325
|
+
return promise
|
|
446
326
|
}
|
|
447
|
-
|
|
327
|
+
|
|
448
328
|
override fun readCharacteristic(
|
|
449
329
|
deviceId: String,
|
|
450
330
|
serviceUUID: String,
|
|
451
331
|
characteristicUUID: String
|
|
452
|
-
):
|
|
332
|
+
): Promise<CharacteristicValue> {
|
|
453
333
|
val gatt = connectedDevices[deviceId]
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
return
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
val
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if (characteristic
|
|
465
|
-
|
|
466
|
-
return
|
|
334
|
+
?: return Promise.rejected(IllegalStateException("Device not connected: $deviceId"))
|
|
335
|
+
val characteristic = findCharacteristic(gatt, serviceUUID, characteristicUUID)
|
|
336
|
+
?: return Promise.rejected(
|
|
337
|
+
IllegalArgumentException("Characteristic not found: $serviceUUID/$characteristicUUID")
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
val promise = Promise<CharacteristicValue>()
|
|
341
|
+
val key = characteristicKey(deviceId, serviceUUID, characteristicUUID)
|
|
342
|
+
pendingReads[key] = promise
|
|
343
|
+
|
|
344
|
+
if (!gatt.readCharacteristic(characteristic)) {
|
|
345
|
+
pendingReads.remove(key)
|
|
346
|
+
return Promise.rejected(IllegalStateException("Failed to start characteristic read"))
|
|
467
347
|
}
|
|
468
|
-
|
|
469
|
-
gatt.readCharacteristic(characteristic)
|
|
470
|
-
|
|
471
|
-
return mapOf(
|
|
472
|
-
"value" to "",
|
|
473
|
-
"serviceUUID" to serviceUUID,
|
|
474
|
-
"characteristicUUID" to characteristicUUID
|
|
475
|
-
)
|
|
348
|
+
return promise
|
|
476
349
|
}
|
|
477
|
-
|
|
350
|
+
|
|
478
351
|
override fun writeCharacteristic(
|
|
479
352
|
deviceId: String,
|
|
480
353
|
serviceUUID: String,
|
|
481
354
|
characteristicUUID: String,
|
|
482
355
|
value: String,
|
|
483
|
-
writeType:
|
|
484
|
-
) {
|
|
356
|
+
writeType: WriteType?
|
|
357
|
+
): Promise<Unit> {
|
|
485
358
|
val gatt = connectedDevices[deviceId]
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
return
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
val
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
if (characteristic == null) {
|
|
497
|
-
Log.e(TAG, "Characteristic not found")
|
|
498
|
-
return
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
val data = hexStringToByteArray(value) ?: return
|
|
359
|
+
?: return Promise.rejected(IllegalStateException("Device not connected: $deviceId"))
|
|
360
|
+
val characteristic = findCharacteristic(gatt, serviceUUID, characteristicUUID)
|
|
361
|
+
?: return Promise.rejected(
|
|
362
|
+
IllegalArgumentException("Characteristic not found: $serviceUUID/$characteristicUUID")
|
|
363
|
+
)
|
|
364
|
+
val data = hexStringToByteArray(value)
|
|
365
|
+
?: return Promise.rejected(IllegalArgumentException("Invalid hex string for characteristic write"))
|
|
366
|
+
|
|
502
367
|
characteristic.value = data
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
BluetoothGattCharacteristic.
|
|
506
|
-
} else {
|
|
507
|
-
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
|
368
|
+
characteristic.writeType = when (writeType) {
|
|
369
|
+
WriteType.WRITEWITHOUTRESPONSE -> BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
370
|
+
else -> BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
|
508
371
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
372
|
+
|
|
373
|
+
val promise = Promise<Unit>()
|
|
374
|
+
val key = characteristicKey(deviceId, serviceUUID, characteristicUUID)
|
|
375
|
+
pendingWrites[key] = promise
|
|
376
|
+
|
|
377
|
+
if (!gatt.writeCharacteristic(characteristic)) {
|
|
378
|
+
pendingWrites.remove(key)
|
|
379
|
+
return Promise.rejected(IllegalStateException("Failed to start characteristic write"))
|
|
380
|
+
}
|
|
381
|
+
return promise
|
|
512
382
|
}
|
|
513
|
-
|
|
383
|
+
|
|
514
384
|
override fun subscribeToCharacteristic(
|
|
515
385
|
deviceId: String,
|
|
516
386
|
serviceUUID: String,
|
|
517
387
|
characteristicUUID: String
|
|
518
388
|
) {
|
|
519
|
-
val gatt = connectedDevices[deviceId]
|
|
520
|
-
|
|
521
|
-
Log.e(TAG, "Device not connected: $deviceId")
|
|
522
|
-
return
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
val characteristics = deviceCharacteristics[deviceId] ?: return
|
|
526
|
-
val characteristic = characteristics.firstOrNull {
|
|
527
|
-
it.service?.uuid.toString() == serviceUUID && it.uuid.toString() == characteristicUUID
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
if (characteristic == null) {
|
|
531
|
-
Log.e(TAG, "Characteristic not found")
|
|
532
|
-
return
|
|
533
|
-
}
|
|
534
|
-
|
|
389
|
+
val gatt = connectedDevices[deviceId] ?: return
|
|
390
|
+
val characteristic = findCharacteristic(gatt, serviceUUID, characteristicUUID) ?: return
|
|
535
391
|
gatt.setCharacteristicNotification(characteristic, true)
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
)
|
|
541
|
-
descriptor?.let {
|
|
542
|
-
it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
|
543
|
-
gatt.writeDescriptor(it)
|
|
392
|
+
|
|
393
|
+
characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID)?.let { descriptor ->
|
|
394
|
+
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
|
395
|
+
gatt.writeDescriptor(descriptor)
|
|
544
396
|
}
|
|
545
397
|
}
|
|
546
|
-
|
|
398
|
+
|
|
547
399
|
override fun unsubscribeFromCharacteristic(
|
|
548
400
|
deviceId: String,
|
|
549
401
|
serviceUUID: String,
|
|
550
402
|
characteristicUUID: String
|
|
551
403
|
) {
|
|
404
|
+
val gatt = connectedDevices[deviceId] ?: return
|
|
405
|
+
val characteristic = findCharacteristic(gatt, serviceUUID, characteristicUUID) ?: return
|
|
406
|
+
gatt.setCharacteristicNotification(characteristic, false)
|
|
407
|
+
|
|
408
|
+
characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID)?.let { descriptor ->
|
|
409
|
+
descriptor.value = BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
|
|
410
|
+
gatt.writeDescriptor(descriptor)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
override fun getConnectedDevices(): Promise<Array<String>> {
|
|
415
|
+
return Promise.resolved(connectedDevices.keys.toTypedArray())
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
override fun readRSSI(deviceId: String): Promise<Double> {
|
|
552
419
|
val gatt = connectedDevices[deviceId]
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
420
|
+
?: return Promise.rejected(IllegalStateException("Device not connected: $deviceId"))
|
|
421
|
+
|
|
422
|
+
lastRssiValues[deviceId]?.let { cachedRssi ->
|
|
423
|
+
return Promise.resolved(cachedRssi)
|
|
556
424
|
}
|
|
557
|
-
|
|
558
|
-
val
|
|
559
|
-
|
|
560
|
-
|
|
425
|
+
|
|
426
|
+
val promise = Promise<Double>()
|
|
427
|
+
pendingRssiReads[deviceId] = promise
|
|
428
|
+
if (!gatt.readRemoteRssi()) {
|
|
429
|
+
pendingRssiReads.remove(deviceId)
|
|
430
|
+
return Promise.rejected(IllegalStateException("Failed to start RSSI read"))
|
|
561
431
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
432
|
+
return promise
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
override fun addListener(eventName: String) {
|
|
436
|
+
// Nitro uses JS-side listener registration. No native bookkeeping required here.
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
override fun removeListeners(count: Double) {
|
|
440
|
+
// Nitro uses JS-side listener registration. No native bookkeeping required here.
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private fun restartAdvertising(delayMs: Long) {
|
|
444
|
+
ensureBluetoothManager()
|
|
445
|
+
val adapter = bluetoothAdapter
|
|
446
|
+
if (adapter == null || !adapter.isEnabled) {
|
|
447
|
+
Log.e(TAG, "Bluetooth is not enabled or not available")
|
|
565
448
|
return
|
|
566
449
|
}
|
|
567
|
-
|
|
568
|
-
|
|
450
|
+
|
|
451
|
+
advertiseJob?.cancel()
|
|
452
|
+
advertiseCallback?.let { callback ->
|
|
453
|
+
advertiser?.stopAdvertising(callback)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
advertiseJob = bluetoothScope.launch {
|
|
457
|
+
if (delayMs > 0) {
|
|
458
|
+
delay(delayMs)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
advertiser = adapter.bluetoothLeAdvertiser
|
|
462
|
+
val activeAdvertiser = advertiser
|
|
463
|
+
if (activeAdvertiser == null) {
|
|
464
|
+
Log.e(TAG, "Bluetooth LE advertiser is not available")
|
|
465
|
+
return@launch
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
val dataBuilder = AdvertiseData.Builder()
|
|
469
|
+
currentAdvertisingData?.let { processAdvertisingData(it, dataBuilder) }
|
|
470
|
+
currentServiceUUIDs.forEach { uuid ->
|
|
471
|
+
dataBuilder.addServiceUuid(ParcelUuid.fromString(uuid))
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
val settings = AdvertiseSettings.Builder()
|
|
475
|
+
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
|
|
476
|
+
.setConnectable(true)
|
|
477
|
+
.setTimeout(0)
|
|
478
|
+
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
|
|
479
|
+
.build()
|
|
480
|
+
|
|
481
|
+
advertiseCallback = object : AdvertiseCallback() {
|
|
482
|
+
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
|
|
483
|
+
Log.i(TAG, "Advertising started successfully")
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
override fun onStartFailure(errorCode: Int) {
|
|
487
|
+
Log.e(TAG, "Advertising failed: $errorCode")
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
activeAdvertiser.startAdvertising(settings, dataBuilder.build(), advertiseCallback)
|
|
492
|
+
}
|
|
569
493
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
return
|
|
494
|
+
|
|
495
|
+
private fun buildGattServerCallback(): BluetoothGattServerCallback {
|
|
496
|
+
return object : BluetoothGattServerCallback() {
|
|
497
|
+
override fun onCharacteristicReadRequest(
|
|
498
|
+
device: BluetoothDevice,
|
|
499
|
+
requestId: Int,
|
|
500
|
+
offset: Int,
|
|
501
|
+
characteristic: BluetoothGattCharacteristic
|
|
502
|
+
) {
|
|
503
|
+
gattServer?.sendResponse(
|
|
504
|
+
device,
|
|
505
|
+
requestId,
|
|
506
|
+
BluetoothGatt.GATT_SUCCESS,
|
|
507
|
+
offset,
|
|
508
|
+
characteristic.value
|
|
509
|
+
)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
override fun onCharacteristicWriteRequest(
|
|
513
|
+
device: BluetoothDevice,
|
|
514
|
+
requestId: Int,
|
|
515
|
+
characteristic: BluetoothGattCharacteristic,
|
|
516
|
+
preparedWrite: Boolean,
|
|
517
|
+
responseNeeded: Boolean,
|
|
518
|
+
offset: Int,
|
|
519
|
+
value: ByteArray?
|
|
520
|
+
) {
|
|
521
|
+
characteristic.value = value ?: byteArrayOf()
|
|
522
|
+
if (responseNeeded) {
|
|
523
|
+
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null)
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
573
527
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
528
|
+
|
|
529
|
+
private fun createGattCallback(deviceId: String): BluetoothGattCallback {
|
|
530
|
+
return object : BluetoothGattCallback() {
|
|
531
|
+
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
|
532
|
+
if (status != BluetoothGatt.GATT_SUCCESS && newState != BluetoothProfile.STATE_CONNECTED) {
|
|
533
|
+
pendingConnections.remove(deviceId)?.reject(
|
|
534
|
+
IllegalStateException("Failed to connect to $deviceId (status=$status)")
|
|
535
|
+
)
|
|
536
|
+
connectedDevices.remove(deviceId)?.close()
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
when (newState) {
|
|
541
|
+
BluetoothProfile.STATE_CONNECTED -> {
|
|
542
|
+
connectedDevices[deviceId] = gatt
|
|
543
|
+
pendingConnections.remove(deviceId)?.resolve(Unit)
|
|
544
|
+
eventEmitter.emit("deviceConnected", mapOf("deviceId" to deviceId))
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
BluetoothProfile.STATE_DISCONNECTED -> {
|
|
548
|
+
pendingConnections.remove(deviceId)?.reject(
|
|
549
|
+
IllegalStateException("Disconnected from $deviceId")
|
|
550
|
+
)
|
|
551
|
+
connectedDevices.remove(deviceId)?.close()
|
|
552
|
+
rejectPendingOperationsForDevice(
|
|
553
|
+
deviceId,
|
|
554
|
+
IllegalStateException("Disconnected from $deviceId")
|
|
555
|
+
)
|
|
556
|
+
eventEmitter.emit("deviceDisconnected", mapOf("deviceId" to deviceId))
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
|
562
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
563
|
+
val services = buildGattServices(gatt)
|
|
564
|
+
pendingServiceDiscoveries.remove(deviceId)?.resolve(services)
|
|
565
|
+
eventEmitter.emit(
|
|
566
|
+
"servicesDiscovered",
|
|
567
|
+
mapOf("deviceId" to deviceId, "services" to services.map { service ->
|
|
568
|
+
mapOf(
|
|
569
|
+
"uuid" to service.uuid,
|
|
570
|
+
"characteristics" to service.characteristics.map { characteristic ->
|
|
571
|
+
mapOf(
|
|
572
|
+
"uuid" to characteristic.uuid,
|
|
573
|
+
"properties" to characteristic.properties.toList(),
|
|
574
|
+
"value" to characteristic.value
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
)
|
|
578
|
+
})
|
|
579
|
+
)
|
|
580
|
+
} else {
|
|
581
|
+
pendingServiceDiscoveries.remove(deviceId)?.reject(
|
|
582
|
+
IllegalStateException("Failed to discover services for $deviceId (status=$status)")
|
|
583
|
+
)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
override fun onCharacteristicRead(
|
|
588
|
+
gatt: BluetoothGatt,
|
|
589
|
+
characteristic: BluetoothGattCharacteristic,
|
|
590
|
+
status: Int
|
|
591
|
+
) {
|
|
592
|
+
val key = characteristicKey(
|
|
593
|
+
deviceId,
|
|
594
|
+
characteristic.service.uuid.toString(),
|
|
595
|
+
characteristic.uuid.toString()
|
|
596
|
+
)
|
|
597
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
598
|
+
val value = buildCharacteristicValue(characteristic)
|
|
599
|
+
lastCharacteristicValues[key] = value
|
|
600
|
+
pendingReads.remove(key)?.resolve(value)
|
|
601
|
+
eventEmitter.emit(
|
|
602
|
+
"characteristicValueChanged",
|
|
603
|
+
mapOf(
|
|
604
|
+
"deviceId" to deviceId,
|
|
605
|
+
"serviceUUID" to value.serviceUUID,
|
|
606
|
+
"characteristicUUID" to value.characteristicUUID,
|
|
607
|
+
"value" to value.value
|
|
608
|
+
)
|
|
609
|
+
)
|
|
610
|
+
} else {
|
|
611
|
+
pendingReads.remove(key)?.reject(
|
|
612
|
+
IllegalStateException("Failed to read characteristic $key (status=$status)")
|
|
613
|
+
)
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
override fun onCharacteristicWrite(
|
|
618
|
+
gatt: BluetoothGatt,
|
|
619
|
+
characteristic: BluetoothGattCharacteristic,
|
|
620
|
+
status: Int
|
|
621
|
+
) {
|
|
622
|
+
val key = characteristicKey(
|
|
623
|
+
deviceId,
|
|
624
|
+
characteristic.service.uuid.toString(),
|
|
625
|
+
characteristic.uuid.toString()
|
|
626
|
+
)
|
|
627
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
628
|
+
pendingWrites.remove(key)?.resolve(Unit)
|
|
629
|
+
} else {
|
|
630
|
+
pendingWrites.remove(key)?.reject(
|
|
631
|
+
IllegalStateException("Failed to write characteristic $key (status=$status)")
|
|
632
|
+
)
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
override fun onCharacteristicChanged(
|
|
637
|
+
gatt: BluetoothGatt,
|
|
638
|
+
characteristic: BluetoothGattCharacteristic
|
|
639
|
+
) {
|
|
640
|
+
val value = buildCharacteristicValue(characteristic)
|
|
641
|
+
val key = characteristicKey(deviceId, value.serviceUUID, value.characteristicUUID)
|
|
642
|
+
lastCharacteristicValues[key] = value
|
|
643
|
+
eventEmitter.emit(
|
|
644
|
+
"characteristicValueChanged",
|
|
645
|
+
mapOf(
|
|
646
|
+
"deviceId" to deviceId,
|
|
647
|
+
"serviceUUID" to value.serviceUUID,
|
|
648
|
+
"characteristicUUID" to value.characteristicUUID,
|
|
649
|
+
"value" to value.value
|
|
650
|
+
)
|
|
651
|
+
)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) {
|
|
655
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
656
|
+
val rssiValue = rssi.toDouble()
|
|
657
|
+
lastRssiValues[deviceId] = rssiValue
|
|
658
|
+
pendingRssiReads.remove(deviceId)?.resolve(rssiValue)
|
|
659
|
+
eventEmitter.emit(
|
|
660
|
+
"rssiUpdated",
|
|
661
|
+
mapOf("deviceId" to deviceId, "rssi" to rssiValue)
|
|
662
|
+
)
|
|
663
|
+
} else {
|
|
664
|
+
pendingRssiReads.remove(deviceId)?.reject(
|
|
665
|
+
IllegalStateException("Failed to read RSSI for $deviceId (status=$status)")
|
|
666
|
+
)
|
|
667
|
+
}
|
|
668
|
+
}
|
|
580
669
|
}
|
|
581
|
-
|
|
582
|
-
gatt.readRemoteRssi()
|
|
583
|
-
// RSSI will come via callback
|
|
584
|
-
return 0.0
|
|
585
670
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
671
|
+
|
|
672
|
+
private fun rejectPendingOperationsForDevice(deviceId: String, error: Throwable) {
|
|
673
|
+
pendingReads.keys
|
|
674
|
+
.filter { it.startsWith("$deviceId|") }
|
|
675
|
+
.forEach { key -> pendingReads.remove(key)?.reject(error) }
|
|
676
|
+
pendingWrites.keys
|
|
677
|
+
.filter { it.startsWith("$deviceId|") }
|
|
678
|
+
.forEach { key -> pendingWrites.remove(key)?.reject(error) }
|
|
679
|
+
pendingServiceDiscoveries.remove(deviceId)?.reject(error)
|
|
680
|
+
pendingRssiReads.remove(deviceId)?.reject(error)
|
|
592
681
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
682
|
+
|
|
683
|
+
private fun buildGattServices(gatt: BluetoothGatt): Array<GATTService> {
|
|
684
|
+
return gatt.services.map { service ->
|
|
685
|
+
GATTService(
|
|
686
|
+
uuid = service.uuid.toString(),
|
|
687
|
+
characteristics = service.characteristics.map { characteristic ->
|
|
688
|
+
GATTCharacteristic(
|
|
689
|
+
uuid = characteristic.uuid.toString(),
|
|
690
|
+
properties = propertiesToArray(characteristic.properties),
|
|
691
|
+
value = characteristic.value?.toHexString()
|
|
692
|
+
)
|
|
693
|
+
}.toTypedArray()
|
|
694
|
+
)
|
|
695
|
+
}.toTypedArray()
|
|
597
696
|
}
|
|
598
|
-
|
|
697
|
+
|
|
698
|
+
private fun buildCharacteristicValue(characteristic: BluetoothGattCharacteristic): CharacteristicValue {
|
|
699
|
+
return CharacteristicValue(
|
|
700
|
+
value = characteristic.value?.toHexString() ?: "",
|
|
701
|
+
serviceUUID = characteristic.service.uuid.toString(),
|
|
702
|
+
characteristicUUID = characteristic.uuid.toString()
|
|
703
|
+
)
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
private fun findCharacteristic(
|
|
707
|
+
gatt: BluetoothGatt,
|
|
708
|
+
serviceUUID: String,
|
|
709
|
+
characteristicUUID: String
|
|
710
|
+
): BluetoothGattCharacteristic? {
|
|
711
|
+
val service = gatt.services.firstOrNull { it.uuid.toString().equals(serviceUUID, ignoreCase = true) }
|
|
712
|
+
?: return null
|
|
713
|
+
return service.characteristics.firstOrNull {
|
|
714
|
+
it.uuid.toString().equals(characteristicUUID, ignoreCase = true)
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
private fun characteristicKey(
|
|
719
|
+
deviceId: String,
|
|
720
|
+
serviceUUID: String,
|
|
721
|
+
characteristicUUID: String
|
|
722
|
+
): String {
|
|
723
|
+
return "$deviceId|${serviceUUID.lowercase()}|${characteristicUUID.lowercase()}"
|
|
724
|
+
}
|
|
725
|
+
|
|
599
726
|
private fun buildScanPayload(result: ScanResult): Map<String, Any?> {
|
|
600
727
|
val record = result.scanRecord
|
|
601
728
|
val manufacturerData = extractManufacturerData(record)
|
|
602
729
|
val serviceUUIDs = record?.serviceUuids?.map { it.uuid.toString() }
|
|
603
730
|
val serviceData = extractServiceData(record)
|
|
604
731
|
val txPower = record?.txPowerLevel?.takeIf { it != Int.MIN_VALUE }
|
|
605
|
-
val
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
732
|
+
val advertisingData = mutableMapOf<String, Any?>()
|
|
733
|
+
|
|
734
|
+
record?.deviceName?.let { advertisingData["completeLocalName"] = it }
|
|
735
|
+
txPower?.let { advertisingData["txPowerLevel"] = it }
|
|
736
|
+
manufacturerData?.let { advertisingData["manufacturerData"] = it }
|
|
737
|
+
serviceUUIDs?.let { advertisingData["serviceUUIDs"] = it }
|
|
738
|
+
serviceData?.takeIf { it.isNotEmpty() }?.let { entries ->
|
|
739
|
+
advertisingData["serviceData"] = entries.map { mapOf("uuid" to it.uuid, "data" to it.data) }
|
|
740
|
+
}
|
|
611
741
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
612
|
-
|
|
742
|
+
advertisingData["isConnectable"] = result.isConnectable
|
|
613
743
|
}
|
|
614
|
-
|
|
615
|
-
|
|
744
|
+
advertisingData["rssi"] = result.rssi
|
|
745
|
+
|
|
616
746
|
return mapOf(
|
|
617
747
|
"id" to result.device.address,
|
|
618
748
|
"name" to result.device.name,
|
|
619
749
|
"localName" to record?.deviceName,
|
|
620
750
|
"manufacturerData" to manufacturerData,
|
|
621
751
|
"serviceUUIDs" to serviceUUIDs,
|
|
622
|
-
"serviceData" to serviceData,
|
|
752
|
+
"serviceData" to serviceData?.map { mapOf("uuid" to it.uuid, "data" to it.data) },
|
|
623
753
|
"rssi" to result.rssi,
|
|
624
754
|
"txPowerLevel" to txPower,
|
|
625
755
|
"isConnectable" to if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) result.isConnectable else null,
|
|
626
|
-
"
|
|
756
|
+
"advertisingData" to advertisingData
|
|
627
757
|
)
|
|
628
758
|
}
|
|
629
|
-
|
|
759
|
+
|
|
630
760
|
private fun extractManufacturerData(record: ScanRecord?): String? {
|
|
631
761
|
val data = record?.manufacturerSpecificData ?: return null
|
|
632
762
|
if (data.size() == 0) return null
|
|
633
|
-
|
|
634
|
-
return bytes.toHexString()
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
private fun extractServiceData(record: ScanRecord?): List<Map<String, Any?>>? {
|
|
638
|
-
val map = record?.serviceData ?: return null
|
|
639
|
-
val result = map.entries.mapNotNull { entry ->
|
|
640
|
-
val bytes = entry.value ?: return@mapNotNull null
|
|
641
|
-
mapOf(
|
|
642
|
-
"uuid" to entry.key.uuid.toString(),
|
|
643
|
-
"data" to bytes.toHexString()
|
|
644
|
-
)
|
|
645
|
-
}
|
|
646
|
-
return result.takeIf { it.isNotEmpty() }
|
|
763
|
+
return data.valueAt(0)?.toHexString()
|
|
647
764
|
}
|
|
648
|
-
|
|
649
|
-
private fun
|
|
650
|
-
|
|
765
|
+
|
|
766
|
+
private fun extractServiceData(record: ScanRecord?): List<ServiceDataEntry>? {
|
|
767
|
+
val data = record?.serviceData ?: return null
|
|
768
|
+
return data.entries.mapNotNull { entry ->
|
|
769
|
+
val value = entry.value ?: return@mapNotNull null
|
|
770
|
+
ServiceDataEntry(entry.key.uuid.toString(), value.toHexString())
|
|
771
|
+
}.takeIf { it.isNotEmpty() }
|
|
651
772
|
}
|
|
652
|
-
|
|
653
|
-
// MARK: - Helper Methods
|
|
654
|
-
|
|
773
|
+
|
|
655
774
|
private fun processAdvertisingData(
|
|
656
|
-
|
|
775
|
+
data: AdvertisingDataTypes,
|
|
657
776
|
dataBuilder: AdvertiseData.Builder
|
|
658
777
|
) {
|
|
659
|
-
|
|
660
|
-
addServiceUUIDs(
|
|
661
|
-
addServiceUUIDs(
|
|
662
|
-
addServiceUUIDs(
|
|
663
|
-
addServiceUUIDs(
|
|
664
|
-
addServiceUUIDs(
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
// Local Name
|
|
668
|
-
if (dataMap.containsKey("shortenedLocalName") || dataMap.containsKey("completeLocalName")) {
|
|
778
|
+
addServiceUUIDs(data.incompleteServiceUUIDs16, dataBuilder)
|
|
779
|
+
addServiceUUIDs(data.completeServiceUUIDs16, dataBuilder)
|
|
780
|
+
addServiceUUIDs(data.incompleteServiceUUIDs32, dataBuilder)
|
|
781
|
+
addServiceUUIDs(data.completeServiceUUIDs32, dataBuilder)
|
|
782
|
+
addServiceUUIDs(data.incompleteServiceUUIDs128, dataBuilder)
|
|
783
|
+
addServiceUUIDs(data.completeServiceUUIDs128, dataBuilder)
|
|
784
|
+
|
|
785
|
+
if (data.shortenedLocalName != null || data.completeLocalName != null) {
|
|
669
786
|
dataBuilder.setIncludeDeviceName(true)
|
|
670
787
|
}
|
|
671
|
-
|
|
672
|
-
// Tx Power Level
|
|
673
|
-
if (dataMap.containsKey("txPowerLevel")) {
|
|
788
|
+
if (data.txPowerLevel != null) {
|
|
674
789
|
dataBuilder.setIncludeTxPowerLevel(true)
|
|
675
790
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
addServiceUUIDs(
|
|
679
|
-
addServiceUUIDs(
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
addServiceData(dataMap["serviceData128"] as? List<Map<String, Any>>, dataBuilder)
|
|
686
|
-
|
|
687
|
-
// Appearance
|
|
688
|
-
if (dataMap.containsKey("appearance")) {
|
|
689
|
-
val appearance = (dataMap["appearance"] as? Number)?.toInt() ?: 0
|
|
791
|
+
|
|
792
|
+
addServiceUUIDs(data.serviceSolicitationUUIDs16, dataBuilder)
|
|
793
|
+
addServiceUUIDs(data.serviceSolicitationUUIDs32, dataBuilder)
|
|
794
|
+
addServiceUUIDs(data.serviceSolicitationUUIDs128, dataBuilder)
|
|
795
|
+
addServiceData(data.serviceData16, dataBuilder)
|
|
796
|
+
addServiceData(data.serviceData32, dataBuilder)
|
|
797
|
+
addServiceData(data.serviceData128, dataBuilder)
|
|
798
|
+
|
|
799
|
+
data.appearance?.toInt()?.let { appearance ->
|
|
690
800
|
val appearanceData = byteArrayOf(
|
|
691
801
|
(appearance and 0xFF).toByte(),
|
|
692
802
|
((appearance shr 8) and 0xFF).toByte()
|
|
@@ -696,75 +806,143 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
696
806
|
appearanceData
|
|
697
807
|
)
|
|
698
808
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
val data = hexStringToByteArray(manufacturerData)
|
|
704
|
-
if (data != null) {
|
|
705
|
-
dataBuilder.addManufacturerData(0x0000, data)
|
|
809
|
+
|
|
810
|
+
data.manufacturerData?.let { manufacturerData ->
|
|
811
|
+
hexStringToByteArray(manufacturerData)?.let { bytes ->
|
|
812
|
+
dataBuilder.addManufacturerData(0x0000, bytes)
|
|
706
813
|
}
|
|
707
814
|
}
|
|
708
815
|
}
|
|
709
|
-
|
|
710
|
-
private fun
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
816
|
+
|
|
817
|
+
private fun normalizeAdvertisingData(
|
|
818
|
+
advertisingData: AdvertisingDataTypes?,
|
|
819
|
+
localName: String?,
|
|
820
|
+
manufacturerData: String?
|
|
821
|
+
): AdvertisingDataTypes {
|
|
822
|
+
val base = advertisingData ?: emptyAdvertisingData()
|
|
823
|
+
return base.copy(
|
|
824
|
+
completeLocalName = base.completeLocalName ?: localName,
|
|
825
|
+
manufacturerData = base.manufacturerData ?: manufacturerData
|
|
826
|
+
)
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
private fun emptyAdvertisingData(): AdvertisingDataTypes {
|
|
830
|
+
return AdvertisingDataTypes(
|
|
831
|
+
flags = null,
|
|
832
|
+
incompleteServiceUUIDs16 = null,
|
|
833
|
+
completeServiceUUIDs16 = null,
|
|
834
|
+
incompleteServiceUUIDs32 = null,
|
|
835
|
+
completeServiceUUIDs32 = null,
|
|
836
|
+
incompleteServiceUUIDs128 = null,
|
|
837
|
+
completeServiceUUIDs128 = null,
|
|
838
|
+
shortenedLocalName = null,
|
|
839
|
+
completeLocalName = null,
|
|
840
|
+
txPowerLevel = null,
|
|
841
|
+
serviceSolicitationUUIDs16 = null,
|
|
842
|
+
serviceSolicitationUUIDs128 = null,
|
|
843
|
+
serviceData16 = null,
|
|
844
|
+
serviceData32 = null,
|
|
845
|
+
serviceData128 = null,
|
|
846
|
+
appearance = null,
|
|
847
|
+
serviceSolicitationUUIDs32 = null,
|
|
848
|
+
manufacturerData = null
|
|
849
|
+
)
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
private fun propertiesFromArray(properties: Array<String>): Int {
|
|
853
|
+
var result = 0
|
|
854
|
+
properties.forEach { property ->
|
|
855
|
+
when (property) {
|
|
856
|
+
"read" -> result = result or BluetoothGattCharacteristic.PROPERTY_READ
|
|
857
|
+
"write" -> result = result or BluetoothGattCharacteristic.PROPERTY_WRITE
|
|
858
|
+
"notify" -> result = result or BluetoothGattCharacteristic.PROPERTY_NOTIFY
|
|
859
|
+
"indicate" -> result = result or BluetoothGattCharacteristic.PROPERTY_INDICATE
|
|
860
|
+
"writeWithoutResponse" -> {
|
|
861
|
+
result = result or BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE
|
|
862
|
+
}
|
|
714
863
|
}
|
|
715
864
|
}
|
|
865
|
+
return result
|
|
716
866
|
}
|
|
717
|
-
|
|
867
|
+
|
|
868
|
+
private fun propertiesToArray(properties: Int): Array<String> {
|
|
869
|
+
val result = mutableListOf<String>()
|
|
870
|
+
if (properties and BluetoothGattCharacteristic.PROPERTY_READ != 0) result += "read"
|
|
871
|
+
if (properties and BluetoothGattCharacteristic.PROPERTY_WRITE != 0) result += "write"
|
|
872
|
+
if (properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0) result += "notify"
|
|
873
|
+
if (properties and BluetoothGattCharacteristic.PROPERTY_INDICATE != 0) result += "indicate"
|
|
874
|
+
if (properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE != 0) {
|
|
875
|
+
result += "writeWithoutResponse"
|
|
876
|
+
}
|
|
877
|
+
return result.toTypedArray()
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
private fun addServiceUUIDs(uuids: Array<String>?, dataBuilder: AdvertiseData.Builder) {
|
|
881
|
+
uuids?.forEach { uuid ->
|
|
882
|
+
dataBuilder.addServiceUuid(ParcelUuid.fromString(uuid))
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
718
886
|
private fun addServiceData(
|
|
719
|
-
|
|
887
|
+
serviceDataEntries: Array<ServiceDataEntry>?,
|
|
720
888
|
dataBuilder: AdvertiseData.Builder
|
|
721
889
|
) {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
val data = serviceData["data"] as? String
|
|
726
|
-
if (uuid != null && data != null) {
|
|
727
|
-
val dataBytes = hexStringToByteArray(data)
|
|
728
|
-
if (dataBytes != null) {
|
|
729
|
-
dataBuilder.addServiceData(ParcelUuid.fromString(uuid), dataBytes)
|
|
730
|
-
}
|
|
731
|
-
}
|
|
890
|
+
serviceDataEntries?.forEach { entry ->
|
|
891
|
+
hexStringToByteArray(entry.data)?.let { dataBytes ->
|
|
892
|
+
dataBuilder.addServiceData(ParcelUuid.fromString(entry.uuid), dataBytes)
|
|
732
893
|
}
|
|
733
894
|
}
|
|
734
895
|
}
|
|
735
|
-
|
|
896
|
+
|
|
736
897
|
private fun hexStringToByteArray(hexString: String?): ByteArray? {
|
|
737
898
|
if (hexString == null) return null
|
|
738
|
-
|
|
899
|
+
|
|
739
900
|
val cleanHex = hexString.replace(" ", "")
|
|
740
901
|
if (cleanHex.length % 2 != 0) return null
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
902
|
+
|
|
903
|
+
return try {
|
|
904
|
+
ByteArray(cleanHex.length / 2).also { bytes ->
|
|
905
|
+
bytes.indices.forEach { index ->
|
|
906
|
+
val offset = index * 2
|
|
907
|
+
bytes[index] = cleanHex.substring(offset, offset + 2).toInt(16).toByte()
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
} catch (_: NumberFormatException) {
|
|
911
|
+
null
|
|
746
912
|
}
|
|
747
|
-
return bytes
|
|
748
913
|
}
|
|
749
|
-
|
|
750
|
-
private fun
|
|
914
|
+
|
|
915
|
+
private fun ByteArray.toHexString(): String {
|
|
916
|
+
return joinToString("") { "%02x".format(it) }
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
private fun setServicesFromOptions(serviceUUIDs: Array<String>) {
|
|
751
920
|
ensureBluetoothManager()
|
|
752
921
|
gattServerReady = false
|
|
753
|
-
|
|
922
|
+
|
|
754
923
|
val manager = bluetoothManager ?: return
|
|
755
|
-
|
|
924
|
+
val context = NitroModules.applicationContext ?: return
|
|
925
|
+
|
|
926
|
+
gattServer?.close()
|
|
927
|
+
gattServer = manager.openGattServer(context, object : BluetoothGattServerCallback() {})
|
|
756
928
|
gattServer?.clearServices()
|
|
757
|
-
|
|
758
|
-
|
|
929
|
+
|
|
930
|
+
serviceUUIDs.forEach { uuid ->
|
|
759
931
|
val service = BluetoothGattService(
|
|
760
932
|
UUID.fromString(uuid),
|
|
761
933
|
BluetoothGattService.SERVICE_TYPE_PRIMARY
|
|
762
934
|
)
|
|
763
935
|
gattServer?.addService(service)
|
|
764
936
|
}
|
|
765
|
-
|
|
937
|
+
|
|
766
938
|
gattServerReady = true
|
|
767
939
|
}
|
|
940
|
+
|
|
941
|
+
companion object {
|
|
942
|
+
private const val TAG = "HybridMunimBluetooth"
|
|
943
|
+
private val CLIENT_CHARACTERISTIC_CONFIG_UUID =
|
|
944
|
+
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
|
|
945
|
+
}
|
|
768
946
|
}
|
|
769
947
|
|
|
770
948
|
private class NitroEventEmitter(private val tag: String) {
|