munim-bluetooth 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,706 @@
1
1
  package com.munimbluetooth
2
2
 
3
3
  import com.margelo.nitro.munimbluetooth.HybridMunimBluetoothSpec
4
+ import android.bluetooth.*
5
+ import android.bluetooth.le.*
6
+ import android.content.Context
7
+ import android.os.ParcelUuid
8
+ import android.util.Log
9
+ import kotlinx.coroutines.*
10
+ import java.util.*
4
11
 
5
- class HybridMunimBluetooth: HybridMunimBluetoothSpec() {
6
- override fun sum(num1: Double, num2: Double): Double {
7
- return num1 + num2
12
+ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
13
+ private val TAG = "HybridMunimBluetooth"
14
+
15
+ // Peripheral Manager
16
+ private var advertiser: BluetoothLeAdvertiser? = null
17
+ private var gattServer: BluetoothGattServer? = null
18
+ private var gattServerReady = false
19
+ private var advertiseJob: Job? = null
20
+ private var currentAdvertisingData: Map<String, Any>? = null
21
+ private var bluetoothManager: BluetoothManager? = null
22
+ private var bluetoothAdapter: BluetoothAdapter? = null
23
+
24
+ // Central Manager
25
+ private var bluetoothLeScanner: BluetoothLeScanner? = null
26
+ private var scanCallback: ScanCallback? = null
27
+ private var isScanning = false
28
+ private val discoveredDevices = mutableMapOf<String, BluetoothDevice>()
29
+ private val connectedDevices = mutableMapOf<String, BluetoothGatt>()
30
+ private val deviceCharacteristics = mutableMapOf<String, MutableList<BluetoothGattCharacteristic>>()
31
+
32
+ init {
33
+ // Initialize Bluetooth managers - this would need ReactApplicationContext in real implementation
34
+ // For now, we'll initialize them when needed
35
+ }
36
+
37
+ private fun getBluetoothManager(): BluetoothManager? {
38
+ // In a real implementation, this would get the context from ReactApplicationContext
39
+ // For now, return null - actual implementation would need context injection
40
+ return null
41
+ }
42
+
43
+ private fun ensureBluetoothManager() {
44
+ if (bluetoothManager == null) {
45
+ bluetoothManager = getBluetoothManager()
46
+ bluetoothAdapter = bluetoothManager?.adapter
47
+ }
48
+ }
49
+
50
+ // MARK: - Peripheral Features
51
+
52
+ override fun startAdvertising(options: Map<String, Any>) {
53
+ ensureBluetoothManager()
54
+ val adapter = bluetoothAdapter
55
+ if (adapter == null || !adapter.isEnabled) {
56
+ Log.e(TAG, "Bluetooth is not enabled or not available")
57
+ return
58
+ }
59
+
60
+ val serviceUUIDs = options["serviceUUIDs"] as? List<String>
61
+ if (serviceUUIDs == null || serviceUUIDs.isEmpty()) {
62
+ Log.e(TAG, "No service UUIDs provided for advertising")
63
+ return
64
+ }
65
+
66
+ // Ensure GATT server is set up before advertising
67
+ if (!gattServerReady) {
68
+ setServicesFromOptions(serviceUUIDs)
69
+ }
70
+
71
+ // Cancel any previous advertising job
72
+ advertiseJob?.cancel()
73
+ advertiseJob = CoroutineScope(Dispatchers.Main).launch {
74
+ delay(300) // Wait for GATT server to be ready
75
+ advertiser = adapter.bluetoothLeAdvertiser
76
+
77
+ val settings = AdvertiseSettings.Builder()
78
+ .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
79
+ .setConnectable(true)
80
+ .setTimeout(0)
81
+ .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
82
+ .build()
83
+
84
+ val dataBuilder = AdvertiseData.Builder()
85
+
86
+ // Process comprehensive advertising data
87
+ val advertisingDataMap = options["advertisingData"] as? Map<String, Any>
88
+ if (advertisingDataMap != null) {
89
+ processAdvertisingData(advertisingDataMap, dataBuilder)
90
+ }
91
+
92
+ // Legacy support - add service UUIDs
93
+ for (uuid in serviceUUIDs) {
94
+ dataBuilder.addServiceUuid(ParcelUuid.fromString(uuid))
95
+ }
96
+
97
+ // Legacy support - local name
98
+ val localName = options["localName"] as? String
99
+ if (localName != null) {
100
+ dataBuilder.setIncludeDeviceName(true)
101
+ }
102
+
103
+ // Legacy support - manufacturer data
104
+ val manufacturerData = options["manufacturerData"] as? String
105
+ if (manufacturerData != null) {
106
+ val data = hexStringToByteArray(manufacturerData)
107
+ if (data != null) {
108
+ dataBuilder.addManufacturerData(0x0000, data) // Default manufacturer code
109
+ }
110
+ }
111
+
112
+ currentAdvertisingData = mapOf(
113
+ "advertisingData" to (advertisingDataMap ?: emptyMap<String, Any>()),
114
+ "localName" to (localName ?: ""),
115
+ "manufacturerData" to (manufacturerData ?: "")
116
+ )
117
+
118
+ advertiser?.startAdvertising(settings, dataBuilder.build(), object : AdvertiseCallback() {
119
+ override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
120
+ Log.i(TAG, "Advertising started successfully")
121
+ }
122
+ override fun onStartFailure(errorCode: Int) {
123
+ Log.e(TAG, "Advertising failed: $errorCode")
124
+ }
125
+ })
126
+ }
127
+ }
128
+
129
+ override fun updateAdvertisingData(advertisingData: Map<String, Any>) {
130
+ ensureBluetoothManager()
131
+ val adapter = bluetoothAdapter
132
+ if (adapter == null || !adapter.isEnabled) {
133
+ Log.e(TAG, "Bluetooth is not enabled or not available")
134
+ return
135
+ }
136
+
137
+ advertiser?.stopAdvertising(object : AdvertiseCallback() {})
138
+
139
+ advertiseJob?.cancel()
140
+ advertiseJob = CoroutineScope(Dispatchers.Main).launch {
141
+ delay(100) // Brief delay before restarting
142
+ advertiser = adapter.bluetoothLeAdvertiser
143
+
144
+ val settings = AdvertiseSettings.Builder()
145
+ .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
146
+ .setConnectable(true)
147
+ .setTimeout(0)
148
+ .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
149
+ .build()
150
+
151
+ val dataBuilder = AdvertiseData.Builder()
152
+ processAdvertisingData(advertisingData, dataBuilder)
153
+
154
+ currentAdvertisingData = mapOf("advertisingData" to advertisingData)
155
+
156
+ advertiser?.startAdvertising(settings, dataBuilder.build(), object : AdvertiseCallback() {
157
+ override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
158
+ Log.i(TAG, "Advertising updated successfully")
159
+ }
160
+ override fun onStartFailure(errorCode: Int) {
161
+ Log.e(TAG, "Advertising update failed: $errorCode")
162
+ }
163
+ })
164
+ }
165
+ }
166
+
167
+ override fun getAdvertisingData(): Map<String, Any> {
168
+ return currentAdvertisingData ?: emptyMap()
169
+ }
170
+
171
+ override fun stopAdvertising() {
172
+ advertiser?.stopAdvertising(object : AdvertiseCallback() {})
173
+ advertiser = null
174
+ advertiseJob?.cancel()
175
+ currentAdvertisingData = null
176
+ }
177
+
178
+ override fun setServices(services: List<Map<String, Any>>) {
179
+ ensureBluetoothManager()
180
+ gattServerReady = false
181
+
182
+ val manager = bluetoothManager ?: return
183
+ gattServer = manager.openGattServer(null, object : BluetoothGattServerCallback() {
184
+ override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) {
185
+ // Handle connection state changes
186
+ }
187
+
188
+ override fun onCharacteristicReadRequest(
189
+ device: BluetoothDevice,
190
+ requestId: Int,
191
+ offset: Int,
192
+ characteristic: BluetoothGattCharacteristic
193
+ ) {
194
+ gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, characteristic.value)
195
+ }
196
+
197
+ override fun onCharacteristicWriteRequest(
198
+ device: BluetoothDevice,
199
+ requestId: Int,
200
+ characteristic: BluetoothGattCharacteristic,
201
+ preparedWrite: Boolean,
202
+ responseNeeded: Boolean,
203
+ offset: Int,
204
+ value: ByteArray?
205
+ ) {
206
+ if (responseNeeded) {
207
+ gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null)
208
+ }
209
+ }
210
+ })
211
+
212
+ gattServer?.clearServices()
213
+
214
+ for (serviceMap in services) {
215
+ val serviceUuid = serviceMap["uuid"] as? String ?: continue
216
+ val service = BluetoothGattService(
217
+ UUID.fromString(serviceUuid),
218
+ BluetoothGattService.SERVICE_TYPE_PRIMARY
219
+ )
220
+
221
+ val characteristics = serviceMap["characteristics"] as? List<Map<String, Any>>
222
+ if (characteristics != null) {
223
+ for (charMap in characteristics) {
224
+ val charUuid = charMap["uuid"] as? String ?: continue
225
+ val propertiesArray = charMap["properties"] as? List<String>
226
+
227
+ var properties = 0
228
+ if (propertiesArray != null) {
229
+ for (prop in propertiesArray) {
230
+ when (prop) {
231
+ "read" -> properties = properties or BluetoothGattCharacteristic.PROPERTY_READ
232
+ "write" -> properties = properties or BluetoothGattCharacteristic.PROPERTY_WRITE
233
+ "notify" -> properties = properties or BluetoothGattCharacteristic.PROPERTY_NOTIFY
234
+ "indicate" -> properties = properties or BluetoothGattCharacteristic.PROPERTY_INDICATE
235
+ }
236
+ }
237
+ }
238
+
239
+ val permissions = BluetoothGattCharacteristic.PERMISSION_READ or
240
+ BluetoothGattCharacteristic.PERMISSION_WRITE
241
+
242
+ val characteristic = BluetoothGattCharacteristic(
243
+ UUID.fromString(charUuid),
244
+ properties,
245
+ permissions
246
+ )
247
+
248
+ val value = charMap["value"] as? String
249
+ if (value != null) {
250
+ characteristic.value = value.toByteArray()
251
+ }
252
+
253
+ service.addCharacteristic(characteristic)
254
+ }
255
+ }
256
+
257
+ gattServer?.addService(service)
258
+ }
259
+
260
+ gattServerReady = true
261
+ }
262
+
263
+ // MARK: - Central Features
264
+
265
+ override fun isBluetoothEnabled(): Boolean {
266
+ ensureBluetoothManager()
267
+ return bluetoothAdapter?.isEnabled == true
268
+ }
269
+
270
+ override fun requestBluetoothPermission(): Boolean {
271
+ // On Android, permissions are requested at runtime
272
+ // This would need Activity context in real implementation
273
+ return true
274
+ }
275
+
276
+ override fun startScan(options: Map<String, Any>?) {
277
+ ensureBluetoothManager()
278
+ val adapter = bluetoothAdapter
279
+ if (adapter == null || !adapter.isEnabled) {
280
+ Log.e(TAG, "Bluetooth is not enabled or not available")
281
+ return
282
+ }
283
+
284
+ if (isScanning) return
285
+
286
+ isScanning = true
287
+ discoveredDevices.clear()
288
+
289
+ val scanner = adapter.bluetoothLeScanner
290
+ bluetoothLeScanner = scanner
291
+
292
+ val serviceUUIDs = options?.get("serviceUUIDs") as? List<String>
293
+ val scanFilters = if (serviceUUIDs != null && serviceUUIDs.isNotEmpty()) {
294
+ serviceUUIDs.map { uuid ->
295
+ ScanFilter.Builder()
296
+ .setServiceUuid(ParcelUuid.fromString(uuid))
297
+ .build()
298
+ }
299
+ } else {
300
+ emptyList()
301
+ }
302
+
303
+ val scanMode = when (options?.get("scanMode") as? String) {
304
+ "lowPower" -> ScanSettings.SCAN_MODE_LOW_POWER
305
+ "balanced" -> ScanSettings.SCAN_MODE_BALANCED
306
+ "lowLatency" -> ScanSettings.SCAN_MODE_LOW_LATENCY
307
+ else -> ScanSettings.SCAN_MODE_BALANCED
308
+ }
309
+
310
+ val scanSettings = ScanSettings.Builder()
311
+ .setScanMode(scanMode)
312
+ .build()
313
+
314
+ scanCallback = object : ScanCallback() {
315
+ override fun onScanResult(callbackType: Int, result: ScanResult) {
316
+ val device = result.device
317
+ val deviceId = device.address
318
+ discoveredDevices[deviceId] = device
319
+
320
+ // Emit deviceFound event
321
+ // In real implementation, this would use event emitter
322
+ }
323
+
324
+ override fun onBatchScanResults(results: MutableList<ScanResult>) {
325
+ for (result in results) {
326
+ onScanResult(ScanCallback.SCAN_RESULT_TYPE_BATCH, result)
327
+ }
328
+ }
329
+
330
+ override fun onScanFailed(errorCode: Int) {
331
+ Log.e(TAG, "Scan failed: $errorCode")
332
+ isScanning = false
333
+ }
334
+ }
335
+
336
+ scanner.startScan(scanFilters, scanSettings, scanCallback)
337
+ }
338
+
339
+ override fun stopScan() {
340
+ if (!isScanning) return
341
+
342
+ bluetoothLeScanner?.stopScan(scanCallback)
343
+ bluetoothLeScanner = null
344
+ scanCallback = null
345
+ isScanning = false
346
+ }
347
+
348
+ override fun connect(deviceId: String) {
349
+ val device = discoveredDevices[deviceId]
350
+ if (device == null) {
351
+ Log.e(TAG, "Device not found: $deviceId")
352
+ return
353
+ }
354
+
355
+ val gatt = device.connectGatt(null, false, object : BluetoothGattCallback() {
356
+ override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
357
+ if (newState == BluetoothProfile.STATE_CONNECTED) {
358
+ connectedDevices[deviceId] = gatt
359
+ gatt.discoverServices()
360
+ // Emit deviceConnected event
361
+ } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
362
+ connectedDevices.remove(deviceId)
363
+ deviceCharacteristics.remove(deviceId)
364
+ // Emit deviceDisconnected event
365
+ }
366
+ }
367
+
368
+ override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
369
+ if (status == BluetoothGatt.GATT_SUCCESS) {
370
+ val characteristics = mutableListOf<BluetoothGattCharacteristic>()
371
+ for (service in gatt.services) {
372
+ characteristics.addAll(service.characteristics)
373
+ }
374
+ deviceCharacteristics[deviceId] = characteristics
375
+ // Emit servicesDiscovered event
376
+ }
377
+ }
378
+
379
+ override fun onCharacteristicRead(
380
+ gatt: BluetoothGatt,
381
+ characteristic: BluetoothGattCharacteristic,
382
+ status: Int
383
+ ) {
384
+ if (status == BluetoothGatt.GATT_SUCCESS) {
385
+ val hexValue = characteristic.value?.joinToString("") { "%02x".format(it) } ?: ""
386
+ // Emit characteristicValueChanged event
387
+ }
388
+ }
389
+
390
+ override fun onCharacteristicWrite(
391
+ gatt: BluetoothGatt,
392
+ characteristic: BluetoothGattCharacteristic,
393
+ status: Int
394
+ ) {
395
+ if (status == BluetoothGatt.GATT_SUCCESS) {
396
+ // Emit writeSuccess event
397
+ } else {
398
+ // Emit writeError event
399
+ }
400
+ }
401
+
402
+ override fun onCharacteristicChanged(
403
+ gatt: BluetoothGatt,
404
+ characteristic: BluetoothGattCharacteristic
405
+ ) {
406
+ val hexValue = characteristic.value?.joinToString("") { "%02x".format(it) } ?: ""
407
+ // Emit characteristicValueChanged event
408
+ }
409
+
410
+ override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) {
411
+ if (status == BluetoothGatt.GATT_SUCCESS) {
412
+ // Emit rssiUpdated event
413
+ }
414
+ }
415
+ })
416
+ }
417
+
418
+ override fun disconnect(deviceId: String) {
419
+ val gatt = connectedDevices[deviceId]
420
+ gatt?.disconnect()
421
+ gatt?.close()
422
+ connectedDevices.remove(deviceId)
423
+ deviceCharacteristics.remove(deviceId)
424
+ }
425
+
426
+ override fun discoverServices(deviceId: String): List<Map<String, Any>> {
427
+ val gatt = connectedDevices[deviceId]
428
+ if (gatt == null) {
429
+ Log.e(TAG, "Device not connected: $deviceId")
430
+ return emptyList()
431
+ }
432
+
433
+ gatt.discoverServices()
434
+
435
+ // Services will be discovered via callback
436
+ // Return empty list for now
437
+ return emptyList()
438
+ }
439
+
440
+ override fun readCharacteristic(
441
+ deviceId: String,
442
+ serviceUUID: String,
443
+ characteristicUUID: String
444
+ ): Map<String, Any> {
445
+ val gatt = connectedDevices[deviceId]
446
+ if (gatt == null) {
447
+ Log.e(TAG, "Device not connected: $deviceId")
448
+ return emptyMap()
449
+ }
450
+
451
+ val characteristics = deviceCharacteristics[deviceId] ?: return emptyMap()
452
+ val characteristic = characteristics.firstOrNull {
453
+ it.service?.uuid.toString() == serviceUUID && it.uuid.toString() == characteristicUUID
454
+ }
455
+
456
+ if (characteristic == null) {
457
+ Log.e(TAG, "Characteristic not found")
458
+ return emptyMap()
459
+ }
460
+
461
+ gatt.readCharacteristic(characteristic)
462
+
463
+ return mapOf(
464
+ "value" to "",
465
+ "serviceUUID" to serviceUUID,
466
+ "characteristicUUID" to characteristicUUID
467
+ )
468
+ }
469
+
470
+ override fun writeCharacteristic(
471
+ deviceId: String,
472
+ serviceUUID: String,
473
+ characteristicUUID: String,
474
+ value: String,
475
+ writeType: String?
476
+ ) {
477
+ val gatt = connectedDevices[deviceId]
478
+ if (gatt == null) {
479
+ Log.e(TAG, "Device not connected: $deviceId")
480
+ return
481
+ }
482
+
483
+ val characteristics = deviceCharacteristics[deviceId] ?: return
484
+ val characteristic = characteristics.firstOrNull {
485
+ it.service?.uuid.toString() == serviceUUID && it.uuid.toString() == characteristicUUID
486
+ }
487
+
488
+ if (characteristic == null) {
489
+ Log.e(TAG, "Characteristic not found")
490
+ return
491
+ }
492
+
493
+ val data = hexStringToByteArray(value) ?: return
494
+ characteristic.value = data
495
+
496
+ val writeTypeValue = if (writeType == "writeWithoutResponse") {
497
+ BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
498
+ } else {
499
+ BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
500
+ }
501
+
502
+ characteristic.writeType = writeTypeValue
503
+ gatt.writeCharacteristic(characteristic)
504
+ }
505
+
506
+ override fun subscribeToCharacteristic(
507
+ deviceId: String,
508
+ serviceUUID: String,
509
+ characteristicUUID: String
510
+ ) {
511
+ val gatt = connectedDevices[deviceId]
512
+ if (gatt == null) {
513
+ Log.e(TAG, "Device not connected: $deviceId")
514
+ return
515
+ }
516
+
517
+ val characteristics = deviceCharacteristics[deviceId] ?: return
518
+ val characteristic = characteristics.firstOrNull {
519
+ it.service?.uuid.toString() == serviceUUID && it.uuid.toString() == characteristicUUID
520
+ }
521
+
522
+ if (characteristic == null) {
523
+ Log.e(TAG, "Characteristic not found")
524
+ return
525
+ }
526
+
527
+ gatt.setCharacteristicNotification(characteristic, true)
528
+
529
+ // Enable notification descriptor
530
+ val descriptor = characteristic.getDescriptor(
531
+ UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
532
+ )
533
+ descriptor?.let {
534
+ it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
535
+ gatt.writeDescriptor(it)
536
+ }
537
+ }
538
+
539
+ override fun unsubscribeFromCharacteristic(
540
+ deviceId: String,
541
+ serviceUUID: String,
542
+ characteristicUUID: String
543
+ ) {
544
+ val gatt = connectedDevices[deviceId]
545
+ if (gatt == null) {
546
+ Log.e(TAG, "Device not connected: $deviceId")
547
+ return
548
+ }
549
+
550
+ val characteristics = deviceCharacteristics[deviceId] ?: return
551
+ val characteristic = characteristics.firstOrNull {
552
+ it.service?.uuid.toString() == serviceUUID && it.uuid.toString() == characteristicUUID
553
+ }
554
+
555
+ if (characteristic == null) {
556
+ Log.e(TAG, "Characteristic not found")
557
+ return
558
+ }
559
+
560
+ gatt.setCharacteristicNotification(characteristic, false)
561
+ }
562
+
563
+ override fun getConnectedDevices(): List<String> {
564
+ return connectedDevices.keys.toList()
565
+ }
566
+
567
+ override fun readRSSI(deviceId: String): Double {
568
+ val gatt = connectedDevices[deviceId]
569
+ if (gatt == null) {
570
+ Log.e(TAG, "Device not connected: $deviceId")
571
+ return 0.0
572
+ }
573
+
574
+ gatt.readRemoteRssi()
575
+ // RSSI will come via callback
576
+ return 0.0
577
+ }
578
+
579
+ // MARK: - Event Management
580
+
581
+ override fun addListener(eventName: String) {
582
+ // Event listeners are handled by the event emitter
583
+ // This is a no-op as Nitro modules handle events differently
584
+ }
585
+
586
+ override fun removeListeners(count: Double) {
587
+ // Event listeners are handled by the event emitter
588
+ // This is a no-op as Nitro modules handle events differently
589
+ }
590
+
591
+ // MARK: - Helper Methods
592
+
593
+ private fun processAdvertisingData(
594
+ dataMap: Map<String, Any>,
595
+ dataBuilder: AdvertiseData.Builder
596
+ ) {
597
+ // Service UUIDs
598
+ addServiceUUIDs(dataMap["incompleteServiceUUIDs16"] as? List<String>, dataBuilder)
599
+ addServiceUUIDs(dataMap["completeServiceUUIDs16"] as? List<String>, dataBuilder)
600
+ addServiceUUIDs(dataMap["incompleteServiceUUIDs32"] as? List<String>, dataBuilder)
601
+ addServiceUUIDs(dataMap["completeServiceUUIDs32"] as? List<String>, dataBuilder)
602
+ addServiceUUIDs(dataMap["incompleteServiceUUIDs128"] as? List<String>, dataBuilder)
603
+ addServiceUUIDs(dataMap["completeServiceUUIDs128"] as? List<String>, dataBuilder)
604
+
605
+ // Local Name
606
+ if (dataMap.containsKey("shortenedLocalName") || dataMap.containsKey("completeLocalName")) {
607
+ dataBuilder.setIncludeDeviceName(true)
608
+ }
609
+
610
+ // Tx Power Level
611
+ if (dataMap.containsKey("txPowerLevel")) {
612
+ dataBuilder.setIncludeTxPowerLevel(true)
613
+ }
614
+
615
+ // Service Solicitation
616
+ addServiceUUIDs(dataMap["serviceSolicitationUUIDs16"] as? List<String>, dataBuilder)
617
+ addServiceUUIDs(dataMap["serviceSolicitationUUIDs128"] as? List<String>, dataBuilder)
618
+ addServiceUUIDs(dataMap["serviceSolicitationUUIDs32"] as? List<String>, dataBuilder)
619
+
620
+ // Service Data
621
+ addServiceData(dataMap["serviceData16"] as? List<Map<String, Any>>, dataBuilder)
622
+ addServiceData(dataMap["serviceData32"] as? List<Map<String, Any>>, dataBuilder)
623
+ addServiceData(dataMap["serviceData128"] as? List<Map<String, Any>>, dataBuilder)
624
+
625
+ // Appearance
626
+ if (dataMap.containsKey("appearance")) {
627
+ val appearance = (dataMap["appearance"] as? Number)?.toInt() ?: 0
628
+ val appearanceData = byteArrayOf(
629
+ (appearance and 0xFF).toByte(),
630
+ ((appearance shr 8) and 0xFF).toByte()
631
+ )
632
+ dataBuilder.addServiceData(
633
+ ParcelUuid.fromString("00001800-0000-1000-8000-00805F9B34FB"),
634
+ appearanceData
635
+ )
636
+ }
637
+
638
+ // Manufacturer Data
639
+ val manufacturerData = dataMap["manufacturerData"] as? String
640
+ if (manufacturerData != null) {
641
+ val data = hexStringToByteArray(manufacturerData)
642
+ if (data != null) {
643
+ dataBuilder.addManufacturerData(0x0000, data)
644
+ }
645
+ }
646
+ }
647
+
648
+ private fun addServiceUUIDs(uuids: List<String>?, dataBuilder: AdvertiseData.Builder) {
649
+ if (uuids != null) {
650
+ for (uuid in uuids) {
651
+ dataBuilder.addServiceUuid(ParcelUuid.fromString(uuid))
652
+ }
653
+ }
654
+ }
655
+
656
+ private fun addServiceData(
657
+ serviceDataArray: List<Map<String, Any>>?,
658
+ dataBuilder: AdvertiseData.Builder
659
+ ) {
660
+ if (serviceDataArray != null) {
661
+ for (serviceData in serviceDataArray) {
662
+ val uuid = serviceData["uuid"] as? String
663
+ val data = serviceData["data"] as? String
664
+ if (uuid != null && data != null) {
665
+ val dataBytes = hexStringToByteArray(data)
666
+ if (dataBytes != null) {
667
+ dataBuilder.addServiceData(ParcelUuid.fromString(uuid), dataBytes)
668
+ }
669
+ }
670
+ }
671
+ }
672
+ }
673
+
674
+ private fun hexStringToByteArray(hexString: String?): ByteArray? {
675
+ if (hexString == null) return null
676
+
677
+ val cleanHex = hexString.replace(" ", "")
678
+ if (cleanHex.length % 2 != 0) return null
679
+
680
+ val bytes = ByteArray(cleanHex.length / 2)
681
+ for (i in bytes.indices) {
682
+ val index = i * 2
683
+ bytes[i] = cleanHex.substring(index, index + 2).toInt(16).toByte()
684
+ }
685
+ return bytes
686
+ }
687
+
688
+ private fun setServicesFromOptions(serviceUUIDs: List<String>) {
689
+ ensureBluetoothManager()
690
+ gattServerReady = false
691
+
692
+ val manager = bluetoothManager ?: return
693
+ gattServer = manager.openGattServer(null, object : BluetoothGattServerCallback() {})
694
+ gattServer?.clearServices()
695
+
696
+ for (uuid in serviceUUIDs) {
697
+ val service = BluetoothGattService(
698
+ UUID.fromString(uuid),
699
+ BluetoothGattService.SERVICE_TYPE_PRIMARY
700
+ )
701
+ gattServer?.addService(service)
702
+ }
703
+
704
+ gattServerReady = true
8
705
  }
9
706
  }