munim-bluetooth 0.3.18 → 0.3.20

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.le.*
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 kotlinx.coroutines.*
16
- import java.util.*
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 TAG = "HybridMunimBluetooth"
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: Map<String, Any>? = null
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 deviceCharacteristics = mutableMapOf<String, MutableList<BluetoothGattCharacteristic>>()
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
- // MARK: - Peripheral Features
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
- // Ensure GATT server is set up before advertising
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: Map<String, Any>) {
136
- ensureBluetoothManager()
137
- val adapter = bluetoothAdapter
138
- if (adapter == null || !adapter.isEnabled) {
139
- Log.e(TAG, "Bluetooth is not enabled or not available")
140
- return
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(): Map<String, Any> {
174
- return currentAdvertisingData ?: emptyMap()
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: List<Map<String, Any>>) {
147
+
148
+ override fun setServices(services: Array<GATTService>) {
185
149
  ensureBluetoothManager()
186
150
  gattServerReady = false
187
-
151
+
188
152
  val manager = bluetoothManager ?: return
189
- gattServer = manager.openGattServer(null, object : BluetoothGattServerCallback() {
190
- override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) {
191
- // Handle connection state changes
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 (serviceMap in services) {
221
- val serviceUuid = serviceMap["uuid"] as? String ?: continue
158
+
159
+ for (serviceData in services) {
222
160
  val service = BluetoothGattService(
223
- UUID.fromString(serviceUuid),
161
+ UUID.fromString(serviceData.uuid),
224
162
  BluetoothGattService.SERVICE_TYPE_PRIMARY
225
163
  )
226
-
227
- val characteristics = serviceMap["characteristics"] as? List<Map<String, Any>>
228
- if (characteristics != null) {
229
- for (charMap in characteristics) {
230
- val charUuid = charMap["uuid"] as? String ?: continue
231
- val propertiesArray = charMap["properties"] as? List<String>
232
-
233
- var properties = 0
234
- if (propertiesArray != null) {
235
- for (prop in propertiesArray) {
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
- // MARK: - Central Features
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
- // On Android, permissions are requested at runtime
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: Map<String, Any>?) {
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 serviceUUIDs = options?.get("serviceUUIDs") as? List<String>
299
- val scanFilters = if (serviceUUIDs != null && serviceUUIDs.isNotEmpty()) {
300
- serviceUUIDs.map { uuid ->
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
- } else {
306
- emptyList()
307
- }
308
-
309
- val scanMode = when (options?.get("scanMode") as? String) {
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
- val deviceId = device.address
324
- discoveredDevices[deviceId] = device
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
- for (result in results) {
334
- onScanResult(ScanCallback.SCAN_RESULT_TYPE_BATCH, result)
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
- bluetoothLeScanner?.stopScan(scanCallback)
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
- val device = discoveredDevices[deviceId]
358
- if (device == null) {
359
- Log.e(TAG, "Device not found: $deviceId")
360
- return
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
- override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) {
419
- if (status == BluetoothGatt.GATT_SUCCESS) {
420
- // Emit rssiUpdated event
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
- val gatt = connectedDevices[deviceId]
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
- connectedDevices.remove(deviceId)
431
- deviceCharacteristics.remove(deviceId)
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): List<Map<String, Any>> {
310
+
311
+ override fun discoverServices(deviceId: String): Promise<Array<GATTService>> {
435
312
  val gatt = connectedDevices[deviceId]
436
- if (gatt == null) {
437
- Log.e(TAG, "Device not connected: $deviceId")
438
- return emptyList()
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
- gatt.discoverServices()
442
-
443
- // Services will be discovered via callback
444
- // Return empty list for now
445
- return emptyList()
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
- ): Map<String, Any> {
332
+ ): Promise<CharacteristicValue> {
453
333
  val gatt = connectedDevices[deviceId]
454
- if (gatt == null) {
455
- Log.e(TAG, "Device not connected: $deviceId")
456
- return emptyMap()
457
- }
458
-
459
- val characteristics = deviceCharacteristics[deviceId] ?: return emptyMap()
460
- val characteristic = characteristics.firstOrNull {
461
- it.service?.uuid.toString() == serviceUUID && it.uuid.toString() == characteristicUUID
462
- }
463
-
464
- if (characteristic == null) {
465
- Log.e(TAG, "Characteristic not found")
466
- return emptyMap()
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: String?
484
- ) {
356
+ writeType: WriteType?
357
+ ): Promise<Unit> {
485
358
  val gatt = connectedDevices[deviceId]
486
- if (gatt == null) {
487
- Log.e(TAG, "Device not connected: $deviceId")
488
- return
489
- }
490
-
491
- val characteristics = deviceCharacteristics[deviceId] ?: return
492
- val characteristic = characteristics.firstOrNull {
493
- it.service?.uuid.toString() == serviceUUID && it.uuid.toString() == characteristicUUID
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
- val writeTypeValue = if (writeType == "writeWithoutResponse") {
505
- BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
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
- characteristic.writeType = writeTypeValue
511
- gatt.writeCharacteristic(characteristic)
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
- if (gatt == null) {
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
- // Enable notification descriptor
538
- val descriptor = characteristic.getDescriptor(
539
- UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
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
- if (gatt == null) {
554
- Log.e(TAG, "Device not connected: $deviceId")
555
- return
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 characteristics = deviceCharacteristics[deviceId] ?: return
559
- val characteristic = characteristics.firstOrNull {
560
- it.service?.uuid.toString() == serviceUUID && it.uuid.toString() == characteristicUUID
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
- if (characteristic == null) {
564
- Log.e(TAG, "Characteristic not found")
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
- gatt.setCharacteristicNotification(characteristic, false)
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
- override fun getConnectedDevices(): List<String> {
572
- return connectedDevices.keys.toList()
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
- override fun readRSSI(deviceId: String): Double {
576
- val gatt = connectedDevices[deviceId]
577
- if (gatt == null) {
578
- Log.e(TAG, "Device not connected: $deviceId")
579
- return 0.0
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
- // MARK: - Event Management
588
-
589
- override fun addListener(eventName: String) {
590
- // Event listeners are handled by the event emitter
591
- // This is a no-op as Nitro modules handle events differently
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
- override fun removeListeners(count: Double) {
595
- // Event listeners are handled by the event emitter
596
- // This is a no-op as Nitro modules handle events differently
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 advertisementData = mutableMapOf<String, Any?>()
606
- record?.deviceName?.let { advertisementData["completeLocalName"] = it }
607
- txPower?.let { advertisementData["txPowerLevel"] = it }
608
- manufacturerData?.let { advertisementData["manufacturerData"] = it }
609
- serviceUUIDs?.let { advertisementData["serviceUUIDs"] = it }
610
- serviceData?.takeIf { it.isNotEmpty() }?.let { advertisementData["serviceData"] = it }
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
- advertisementData["isConnectable"] = result.isConnectable
742
+ advertisingData["isConnectable"] = result.isConnectable
613
743
  }
614
- advertisementData["rssi"] = result.rssi
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
- "advertisementData" to advertisementData
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
- val bytes = data.valueAt(0) ?: return null
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 ByteArray.toHexString(): String {
650
- return joinToString("") { "%02x".format(it) }
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
- dataMap: Map<String, Any>,
775
+ data: AdvertisingDataTypes,
657
776
  dataBuilder: AdvertiseData.Builder
658
777
  ) {
659
- // Service UUIDs
660
- addServiceUUIDs(dataMap["incompleteServiceUUIDs16"] as? List<String>, dataBuilder)
661
- addServiceUUIDs(dataMap["completeServiceUUIDs16"] as? List<String>, dataBuilder)
662
- addServiceUUIDs(dataMap["incompleteServiceUUIDs32"] as? List<String>, dataBuilder)
663
- addServiceUUIDs(dataMap["completeServiceUUIDs32"] as? List<String>, dataBuilder)
664
- addServiceUUIDs(dataMap["incompleteServiceUUIDs128"] as? List<String>, dataBuilder)
665
- addServiceUUIDs(dataMap["completeServiceUUIDs128"] as? List<String>, dataBuilder)
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
- // Service Solicitation
678
- addServiceUUIDs(dataMap["serviceSolicitationUUIDs16"] as? List<String>, dataBuilder)
679
- addServiceUUIDs(dataMap["serviceSolicitationUUIDs128"] as? List<String>, dataBuilder)
680
- addServiceUUIDs(dataMap["serviceSolicitationUUIDs32"] as? List<String>, dataBuilder)
681
-
682
- // Service Data
683
- addServiceData(dataMap["serviceData16"] as? List<Map<String, Any>>, dataBuilder)
684
- addServiceData(dataMap["serviceData32"] as? List<Map<String, Any>>, dataBuilder)
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
- // Manufacturer Data
701
- val manufacturerData = dataMap["manufacturerData"] as? String
702
- if (manufacturerData != null) {
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 addServiceUUIDs(uuids: List<String>?, dataBuilder: AdvertiseData.Builder) {
711
- if (uuids != null) {
712
- for (uuid in uuids) {
713
- dataBuilder.addServiceUuid(ParcelUuid.fromString(uuid))
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
- serviceDataArray: List<Map<String, Any>>?,
887
+ serviceDataEntries: Array<ServiceDataEntry>?,
720
888
  dataBuilder: AdvertiseData.Builder
721
889
  ) {
722
- if (serviceDataArray != null) {
723
- for (serviceData in serviceDataArray) {
724
- val uuid = serviceData["uuid"] as? String
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
- val bytes = ByteArray(cleanHex.length / 2)
743
- for (i in bytes.indices) {
744
- val index = i * 2
745
- bytes[i] = cleanHex.substring(index, index + 2).toInt(16).toByte()
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 setServicesFromOptions(serviceUUIDs: List<String>) {
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
- gattServer = manager.openGattServer(null, object : BluetoothGattServerCallback() {})
924
+ val context = NitroModules.applicationContext ?: return
925
+
926
+ gattServer?.close()
927
+ gattServer = manager.openGattServer(context, object : BluetoothGattServerCallback() {})
756
928
  gattServer?.clearServices()
757
-
758
- for (uuid in serviceUUIDs) {
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) {