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.
- package/README.md +769 -15
- package/android/src/main/AndroidManifest.xml +18 -0
- package/android/src/main/java/com/munimbluetooth/HybridMunimBluetooth.kt +700 -3
- package/ios/HybridMunimBluetooth.swift +622 -3
- package/lib/commonjs/index.js +247 -2
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +227 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +161 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/munim-bluetooth.nitro.d.ts +193 -1
- package/lib/typescript/src/specs/munim-bluetooth.nitro.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/index.ts +287 -3
- package/src/specs/munim-bluetooth.nitro.ts +266 -3
|
@@ -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
|
-
|
|
7
|
-
|
|
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
|
}
|