package com.sdkble import android.Manifest import android.annotation.SuppressLint import android.bluetooth.* import android.bluetooth.le.* import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.os.Handler import android.os.Looper import android.os.ParcelUuid import android.util.Log import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule import com.facebook.react.modules.core.DeviceEventManagerModule import java.util.* import java.util.concurrent.ConcurrentHashMap @ReactModule(name = SdkBleModule.NAME) class SdkBleModule(private val reactContext: ReactApplicationContext) : NativeSdkBleSpec(reactContext) { private val bluetoothManager: BluetoothManager by lazy { reactContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager } private val bluetoothAdapter: BluetoothAdapter? by lazy { bluetoothManager.adapter } private val bluetoothLeScanner: BluetoothLeScanner? by lazy { bluetoothAdapter?.bluetoothLeScanner } private val connectedDevices = ConcurrentHashMap() private val discoveredDevices = ConcurrentHashMap() private var isScanning = false // Map of services to characteristics to enable notifications for private val serviceCharacteristicMap = mapOf( UUID.fromString("0000180d-0000-1000-8000-00805f9b34fb") to listOf(UUID.fromString("00002a37-0000-1000-8000-00805f9b34fb")), // Heart Rate UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb") to listOf(UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb")), // Battery UUID.fromString("0000ff00-0000-1000-8000-00805f9b34fb") to listOf(UUID.fromString("0000ff01-0000-1000-8000-00805f9b34fb"), UUID.fromString("0000ff02-0000-1000-8000-00805f9b34fb")), UUID.fromString("0000ff10-0000-1000-8000-00805f9b34fb") to listOf(UUID.fromString("0000ff11-0000-1000-8000-00805f9b34fb")), // Authentication UUID.fromString("0000ff20-0000-1000-8000-00805f9b34fb") to listOf(UUID.fromString("0000ff21-0000-1000-8000-00805f9b34fb")), UUID.fromString("0000ff30-0000-1000-8000-00805f9b34fb") to listOf(UUID.fromString("0000ff31-0000-1000-8000-00805f9b34fb")), UUID.fromString("0000ff40-0000-1000-8000-00805f9b34fb") to listOf(UUID.fromString("0000ff41-0000-1000-8000-00805f9b34fb")) ) // Queue entry for notification/read action private data class CharAction( val characteristic: BluetoothGattCharacteristic, var notifyPending: Boolean, var readPending: Boolean, var readAttempted: Boolean = false ) private val charActionQueue = mutableListOf() private var gattForNotification: BluetoothGatt? = null private val pollHandler = Handler(Looper.getMainLooper()) private var pollIntervalMs: Long = 5000 // 5 seconds, adjust as needed (increased to reduce interference with notifications) private var pollingGatt: BluetoothGatt? = null private var pollingActive = false // --- Sequential polling queue for characteristics --- private val pollingQueue = mutableListOf>() private var pollingInProgress = false // --- Buffer for accumulating ff31 data chunks --- private data class ChunkBuffer( val structId: Int, val chunks: MutableList = mutableListOf(), var lastUpdateTime: Long = System.currentTimeMillis() ) private val ff31ChunkBuffers = ConcurrentHashMap() private val chunkTimeoutMs = 5000L // 5 seconds timeout // Authentication state private var authenticationPending = false private var serialNumberPending = false private var deviceTypePending = false private var authenticationCharacteristic: BluetoothGattCharacteristic? = null private var authenticationGatt: BluetoothGatt? = null // User role for sending role command on connection private var userRole: String = "patient" // Default role override fun getName(): String = NAME // Scanning methods override fun startScan(options: ReadableMap?, promise: Promise) { if (!hasRequiredPermissions()) { promise.reject(Exception("BLE permissions not granted")) return } if (bluetoothAdapter?.isEnabled != true) { promise.reject(Exception("Bluetooth is not enabled")) return } if (isScanning) { promise.resolve(null) return } try { val scanSettings = ScanSettings.Builder() .setScanMode(getScanMode(options?.getString("scanMode"))) .build() val scanFilters = mutableListOf() options?.getArray("serviceUUIDs")?.let { uuids -> for (i in 0 until uuids.size()) { val uuid = uuids.getString(i) scanFilters.add( ScanFilter.Builder() .setServiceUuid(ParcelUuid.fromString(uuid)) .build() ) } } bluetoothLeScanner?.startScan(scanFilters, scanSettings, scanCallback) isScanning = true // Handle timeout val timeout = options?.getDouble("timeout")?.toLong() ?: 10000L if (timeout > 0) { reactContext.runOnUiQueueThread { android.os.Handler().postDelayed({ if (isScanning) { stopScanInternal() } }, timeout) } } promise.resolve(null) } catch (e: Exception) { promise.reject(e) } } override fun stopScan(promise: Promise) { try { stopScanInternal() promise.resolve(null) } catch (e: Exception) { promise.reject(e) } } private fun stopScanInternal() { if (isScanning) { bluetoothLeScanner?.stopScan(scanCallback) isScanning = false } } // Connection methods override fun connect(deviceId: String, promise: Promise) { val device = discoveredDevices[deviceId] ?: bluetoothAdapter?.getRemoteDevice(deviceId) if (device == null) { promise.reject(Exception("Device not found: $deviceId")) return } try { if (connectedDevices.containsKey(deviceId)) { promise.resolve(null) } else { device.connectGatt(reactContext, false, gattCallback) promise.resolve(null) } } catch (e: Exception) { promise.reject(e) } } override fun disconnect(deviceId: String, promise: Promise) { try { val gatt = connectedDevices[deviceId] if (gatt != null) { gatt.disconnect() // Don't call close() immediately - wait for STATE_DISCONNECTED callback } promise.resolve(null) } catch (e: Exception) { promise.reject(e) } } override fun isConnected(deviceId: String, promise: Promise) { promise.resolve(connectedDevices.containsKey(deviceId)) } override fun getConnectedDevices(promise: Promise) { val devices = Arguments.createArray() connectedDevices.forEach { (deviceId, gatt) -> val device = Arguments.createMap().apply { putString("id", deviceId) putString("name", gatt.device.name ?: "Unknown") } devices.pushMap(device) } promise.resolve(devices) } // Service discovery override fun discoverServices(deviceId: String, promise: Promise) { val gatt = connectedDevices[deviceId] if (gatt == null) { promise.reject(Exception("Device not connected: $deviceId")) return } try { if (gatt.discoverServices()) { promise.resolve(Arguments.createArray()) } else { promise.reject(Exception("Failed to start service discovery")) } } catch (e: Exception) { promise.reject(e) } } // Characteristic operations override fun readCharacteristic( deviceId: String, serviceUuid: String, characteristicUuid: String, promise: Promise ) { val gatt = connectedDevices[deviceId] if (gatt == null) { promise.reject(Exception("Device not connected: $deviceId")) return } try { val service = gatt.getService(UUID.fromString(serviceUuid)) if (service == null) { promise.reject(Exception("Service not found: $serviceUuid")) return } val characteristic = service.getCharacteristic(UUID.fromString(characteristicUuid)) if (characteristic == null) { promise.reject(Exception("Characteristic not found: $characteristicUuid")) return } if (gatt.readCharacteristic(characteristic)) { promise.resolve(null) } else { promise.reject(Exception("Failed to read characteristic")) } } catch (e: Exception) { promise.reject(e) } } override fun writeCharacteristic( deviceId: String, serviceUuid: String, characteristicUuid: String, data: String, options: ReadableMap?, promise: Promise ) { val gatt = connectedDevices[deviceId] if (gatt == null) { promise.reject(Exception("Device not connected: $deviceId")) return } try { val service = gatt.getService(UUID.fromString(serviceUuid)) if (service == null) { promise.reject(Exception("Service not found: $serviceUuid")) return } val characteristic = service.getCharacteristic(UUID.fromString(characteristicUuid)) if (characteristic == null) { promise.reject(Exception("Characteristic not found: $characteristicUuid")) return } val dataBytes = if (options?.getString("encoding") == "base64") { android.util.Base64.decode(data, android.util.Base64.DEFAULT) } else { data.toByteArray() } characteristic.value = dataBytes val writeType = when (options?.getString("type")) { "withResponse" -> BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT "withoutResponse" -> BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE else -> BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT } characteristic.writeType = writeType if (gatt.writeCharacteristic(characteristic)) { promise.resolve(null) } else { promise.reject(Exception("Failed to write characteristic")) } } catch (e: Exception) { promise.reject(e) } } override fun setNotify( deviceId: String, serviceUuid: String, characteristicUuid: String, enable: Boolean, promise: Promise ) { val gatt = connectedDevices[deviceId] if (gatt == null) { promise.reject(Exception("Device not connected: $deviceId")) return } try { val service = gatt.getService(UUID.fromString(serviceUuid)) if (service == null) { promise.reject(Exception("Service not found: $serviceUuid")) return } val characteristic = service.getCharacteristic(UUID.fromString(characteristicUuid)) if (characteristic == null) { promise.reject(Exception("Characteristic not found: $characteristicUuid")) return } val success = gatt.setCharacteristicNotification(characteristic, enable) if (success) { // Configure descriptor for notifications val descriptor = characteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")) if (descriptor != null) { val value = if (enable) { BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE } else { BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE } descriptor.value = value gatt.writeDescriptor(descriptor) } promise.resolve(null) } else { promise.reject(Exception("Failed to set notification")) } } catch (e: Exception) { promise.reject(e) } } // Permission and Bluetooth state override fun requestPermissions(promise: Promise) { if (hasRequiredPermissions()) { promise.resolve("granted") } else { promise.resolve("denied") } } override fun isBluetoothEnabled(promise: Promise) { promise.resolve(bluetoothAdapter?.isEnabled == true) } override fun enableBluetooth(promise: Promise) { try { val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) reactContext.startActivity(intent) promise.resolve(null) } catch (e: Exception) { promise.reject(e) } } // Convert Base64 to Hex-Dash string @ReactMethod override fun base64ToHexDash(base64: String, promise: Promise) { try { val bytes = android.util.Base64.decode(base64, android.util.Base64.DEFAULT) val hex = bytes.joinToString("-") { String.format("%02X", it) } promise.resolve("(0x) $hex") } catch (e: Exception) { promise.reject("decode_error", "Failed to decode base64: ${e.message}") } } // Infusion control methods @ReactMethod override fun startInfusion(deviceId: String, promise: Promise) { val gatt = connectedDevices[deviceId] if (gatt == null) { promise.reject(Exception("Device not connected: $deviceId")) return } try { val serviceUuid = UUID.fromString("0000ff10-0000-1000-8000-00805f9b34fb") val characteristicUuid = UUID.fromString("0000ff11-0000-1000-8000-00805f9b34fb") val service = gatt.getService(serviceUuid) if (service == null) { promise.reject(Exception("Service FF10 not found")) return } val characteristic = service.getCharacteristic(characteristicUuid) if (characteristic == null) { promise.reject(Exception("Characteristic FF11 not found")) return } // Command: A4 01 01 val command = byteArrayOf(0xA4.toByte(), 0x01.toByte(), 0x01.toByte()) characteristic.value = command characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT if (gatt.writeCharacteristic(characteristic)) { Log.d("SdkBleModule", "Start infusion command sent: A4 01 01") promise.resolve(null) } else { promise.reject(Exception("Failed to write start infusion command")) } } catch (e: Exception) { promise.reject(e) } } @ReactMethod override fun stopInfusion(deviceId: String, promise: Promise) { val gatt = connectedDevices[deviceId] if (gatt == null) { promise.reject(Exception("Device not connected: $deviceId")) return } try { val serviceUuid = UUID.fromString("0000ff10-0000-1000-8000-00805f9b34fb") val characteristicUuid = UUID.fromString("0000ff11-0000-1000-8000-00805f9b34fb") val service = gatt.getService(serviceUuid) if (service == null) { promise.reject(Exception("Service FF10 not found")) return } val characteristic = service.getCharacteristic(characteristicUuid) if (characteristic == null) { promise.reject(Exception("Characteristic FF11 not found")) return } // Command: A4 01 00 val command = byteArrayOf(0xA4.toByte(), 0x01.toByte(), 0x00.toByte()) characteristic.value = command characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT if (gatt.writeCharacteristic(characteristic)) { Log.d("SdkBleModule", "Stop infusion command sent: A4 01 00") promise.resolve(null) } else { promise.reject(Exception("Failed to write stop infusion command")) } } catch (e: Exception) { promise.reject(e) } } @ReactMethod override fun setInfusionLevel(deviceId: String, level: Double, promise: Promise) { val gatt = connectedDevices[deviceId] if (gatt == null) { promise.reject(Exception("Device not connected: $deviceId")) return } // Validate level (1-5) val levelInt = level.toInt() if (levelInt < 1 || levelInt > 5) { promise.reject(Exception("Invalid level: $levelInt. Level must be between 1 and 5")) return } try { val serviceUuid = UUID.fromString("0000ff10-0000-1000-8000-00805f9b34fb") val characteristicUuid = UUID.fromString("0000ff11-0000-1000-8000-00805f9b34fb") val service = gatt.getService(serviceUuid) if (service == null) { promise.reject(Exception("Service FF10 not found")) return } val characteristic = service.getCharacteristic(characteristicUuid) if (characteristic == null) { promise.reject(Exception("Characteristic FF11 not found")) return } // Command: A5 01 [level] // Level 1: A5 01 01 // Level 2: A5 01 02 // Level 3: A5 01 03 // Level 4: A5 01 04 // Level 5: A5 01 05 val command = byteArrayOf(0xA5.toByte(), 0x01.toByte(), levelInt.toByte()) characteristic.value = command characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT if (gatt.writeCharacteristic(characteristic)) { Log.d("SdkBleModule", "Set infusion level $levelInt command sent: A5 01 ${String.format("%02X", levelInt)}") promise.resolve(null) } else { promise.reject(Exception("Failed to write infusion level command")) } } catch (e: Exception) { promise.reject(e) } } @ReactMethod override fun setUserRole(role: String, promise: Promise) { try { // Validate role val validRoles = listOf("patient", "nurse", "engineer") val roleLower = role.lowercase() if (!validRoles.contains(roleLower)) { promise.reject(Exception("Invalid role: $role. Must be 'patient', 'nurse', or 'engineer'")) return } userRole = roleLower Log.d("SdkBleModule", "User role set to: $userRole") promise.resolve(null) } catch (e: Exception) { promise.reject(e) } } private fun sendRoleCommand(gatt: BluetoothGatt) { try { val serviceUuid = UUID.fromString("0000ff10-0000-1000-8000-00805f9b34fb") val characteristicUuid = UUID.fromString("0000ff11-0000-1000-8000-00805f9b34fb") val service = gatt.getService(serviceUuid) if (service == null) { Log.d("SdkBleModule", "Service FF10 not found for role command") return } val characteristic = service.getCharacteristic(characteristicUuid) if (characteristic == null) { Log.d("SdkBleModule", "Characteristic FF11 not found for role command") return } // Map role to command byte // patient: A3 01 01 // nurse: A3 01 02 // engineer: A3 01 03 val roleByte: Byte = when (userRole) { "patient" -> 0x01.toByte() "nurse" -> 0x02.toByte() "engineer" -> 0x03.toByte() else -> 0x01.toByte() // Default to patient } val command = byteArrayOf(0xA3.toByte(), 0x01.toByte(), roleByte) characteristic.value = command characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT if (gatt.writeCharacteristic(characteristic)) { Log.d("SdkBleModule", "Role command sent for $userRole: A3 01 ${String.format("%02X", roleByte)}") } else { Log.d("SdkBleModule", "Failed to write role command") } } catch (e: Exception) { Log.e("SdkBleModule", "Error sending role command: ${e.message}") } } // Helper methods private fun hasRequiredPermissions(): Boolean { val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { arrayOf( Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT ) } else { arrayOf( Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.ACCESS_FINE_LOCATION ) } return permissions.all { ContextCompat.checkSelfPermission(reactContext, it) == PackageManager.PERMISSION_GRANTED } } private fun getScanMode(mode: String?): Int { return when (mode) { "lowPower" -> ScanSettings.SCAN_MODE_LOW_POWER "balanced" -> ScanSettings.SCAN_MODE_BALANCED "lowLatency" -> ScanSettings.SCAN_MODE_LOW_LATENCY else -> ScanSettings.SCAN_MODE_LOW_POWER } } // Helper to clean up old chunk buffers private fun cleanupOldChunkBuffers() { val currentTime = System.currentTimeMillis() val keysToRemove = mutableListOf() ff31ChunkBuffers.forEach { (structId, buffer) -> if (currentTime - buffer.lastUpdateTime > chunkTimeoutMs) { keysToRemove.add(structId) } } keysToRemove.forEach { structId -> ff31ChunkBuffers.remove(structId) Log.d("SdkBleModule", "Cleaned up expired chunk buffer for struct_id=$structId") } } // Helper to parse BLE notification data private fun parseBleNotification(data: ByteArray, characteristicUuid: String? = null): WritableMap { val map = Arguments.createMap() if (data.size < 11) return map // Not enough data for device state map.putInt("struct_id", data[0].toInt() and 0xFF) map.putInt("total_len", data[1].toInt() and 0xFF) map.putInt("rgb_en", data[2].toInt() and 0xFF) map.putInt("rgb_color", data[3].toInt() and 0xFF) map.putInt("rgb_action", data[4].toInt() and 0xFF) map.putInt("dled_en", data[5].toInt() and 0xFF) map.putInt("dled_mask", data[6].toInt() and 0xFF) map.putString("dled_mask_bin", String.format("%8s", Integer.toBinaryString(data[6].toInt() and 0xFF)).replace(' ', '0')) map.putInt("dled_action", data[7].toInt() and 0xFF) map.putInt("buzzer_en", data[8].toInt() and 0xFF) map.putInt("buzzer_action", data[9].toInt() and 0xFF) map.putInt("device_state", data[10].toInt() and 0xFF) return map } // Helper to parse FF02 data (3 bytes: struct_id, total_len, value) private fun parseFF02Data(data: ByteArray): WritableMap { val map = Arguments.createMap() if (data.size < 3) return map // Not enough data // Extract bytes val structId = data[0].toInt() and 0xFF val totalLen = data[1].toInt() and 0xFF val value = data[2].toInt() and 0xFF // Convert hex to decimal map.putInt("struct_id", structId) map.putInt("total_len", totalLen) map.putInt("value", value) // Add hex representation for debugging map.putString("value_hex", String.format("%02X", value)) return map } // Helper to parse FF41 infusion data private fun parseFF41Data(data: ByteArray): WritableMap { val map = Arguments.createMap() if (data.size < 18) return map // Not enough data // Extract struct_id and total_len val structId = data[0].toInt() and 0xFF val totalLen = data[1].toInt() and 0xFF map.putInt("struct_id", structId) map.putInt("total_len", totalLen) // Parse 4-byte fields with little-endian to big-endian conversion // infusion_state: bytes 2-5 (reversed) val infusionState = ((data[5].toInt() and 0xFF) shl 24) or ((data[4].toInt() and 0xFF) shl 16) or ((data[3].toInt() and 0xFF) shl 8) or (data[2].toInt() and 0xFF) // infusion_duration: bytes 6-9 (reversed) val infusionDuration = ((data[9].toInt() and 0xFF) shl 24) or ((data[8].toInt() and 0xFF) shl 16) or ((data[7].toInt() and 0xFF) shl 8) or (data[6].toInt() and 0xFF) // infusion_pressure: bytes 10-13 (reversed) val infusionPressure = ((data[13].toInt() and 0xFF) shl 24) or ((data[12].toInt() and 0xFF) shl 16) or ((data[11].toInt() and 0xFF) shl 8) or (data[10].toInt() and 0xFF) // infusion_level: bytes 14-17 (reversed) val infusionLevel = ((data[17].toInt() and 0xFF) shl 24) or ((data[16].toInt() and 0xFF) shl 16) or ((data[15].toInt() and 0xFF) shl 8) or (data[14].toInt() and 0xFF) map.putInt("infusion_state", infusionState) map.putInt("infusion_duration", infusionDuration) // Already in milliseconds map.putDouble("infusion_pressure", infusionPressure / 1000.0) // Divide by 1000 map.putInt("infusion_level", infusionLevel) // Add hex representations for debugging map.putString("infusion_state_hex", String.format("%08X", infusionState)) map.putString("infusion_duration_hex", String.format("%08X", infusionDuration)) map.putString("infusion_pressure_hex", String.format("%08X", infusionPressure)) map.putString("infusion_level_hex", String.format("%08X", infusionLevel)) return map } // Callbacks private val scanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { val device = result.device val scanRecord = result.scanRecord var hasEMED = false if (scanRecord != null) { for (i in 0 until scanRecord.manufacturerSpecificData.size()) { val mfgData = scanRecord.getManufacturerSpecificData(scanRecord.manufacturerSpecificData.keyAt(i)) if (mfgData != null && mfgData.size >= 4) { // Search for 'E','M','E','D' anywhere in mfgData for (j in 0..(mfgData.size - 4)) { if (mfgData[j].toInt() == 0x45 && mfgData[j+1].toInt() == 0x4D && mfgData[j+2].toInt() == 0x45 && mfgData[j+3].toInt() == 0x44) { hasEMED = true break } } } if (hasEMED) break } } if (!hasEMED) { return // Skip devices not containing 'E','M','E','D' } discoveredDevices[device.address] = device // Prefer advertised device name over cached name for consistency with iOS val deviceName = scanRecord?.deviceName ?: device.name ?: "Unknown" val deviceMap = Arguments.createMap().apply { putString("id", device.address) putString("name", deviceName) putInt("rssi", result.rssi) } val eventData = Arguments.createMap().apply { putMap("device", deviceMap) } sendEvent("scanResult", eventData) } override fun onScanFailed(errorCode: Int) { val error = Arguments.createMap().apply { putString("error", "Scan failed with code: $errorCode") } sendEvent("BleManagerScanFailed", error) } } private val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { val deviceId = gatt.device.address when (newState) { BluetoothProfile.STATE_CONNECTED -> { connectedDevices[deviceId] = gatt val params = Arguments.createMap().apply { putString("deviceId", deviceId) } sendEvent("connected", params) // Always start service discovery after connection gatt.discoverServices() } BluetoothProfile.STATE_DISCONNECTED -> { connectedDevices.remove(deviceId) stopPolling() // Close the GATT connection to release resources gatt.close() Log.d("SdkBleModule", "GATT connection closed for device: $deviceId") // Clear action queue charActionQueue.clear() gattForNotification = null Log.d("SdkBleModule", "Cleared action queue on disconnect") // Clear chunk buffers on disconnect ff31ChunkBuffers.clear() Log.d("SdkBleModule", "Cleared chunk buffers on disconnect") // Clear authentication state on disconnect authenticationPending = false serialNumberPending = false deviceTypePending = false authenticationCharacteristic = null authenticationGatt = null Log.d("SdkBleModule", "Cleared authentication state on disconnect") val params = Arguments.createMap().apply { putString("deviceId", deviceId) putString("reason", "Disconnected") } sendEvent("disconnected", params) } } } override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { Log.d("SdkBleModule", "onServicesDiscovered called, status: $status") if (status == BluetoothGatt.GATT_SUCCESS) { val services = Arguments.createArray() gatt.services.forEach { service -> Log.d("SdkBleModule", "Found service: ${service.uuid}") val serviceMap = Arguments.createMap().apply { putString("uuid", service.uuid.toString()) val characteristics = Arguments.createArray() service.characteristics?.forEach { characteristic -> Log.d("SdkBleModule", " Found characteristic: ${characteristic.uuid}, properties: ${characteristic.properties}") val charMap = Arguments.createMap().apply { putString("uuid", characteristic.uuid.toString()) val properties = Arguments.createArray() if (characteristic.properties and BluetoothGattCharacteristic.PROPERTY_READ != 0) { properties.pushString("read") } if (characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE != 0) { properties.pushString("write") } if (characteristic.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0) { properties.pushString("notify") } if (characteristic.properties and BluetoothGattCharacteristic.PROPERTY_INDICATE != 0) { properties.pushString("indicate") } putArray("properties", properties) } characteristics.pushMap(charMap) } putArray("characteristics", characteristics) } services.pushMap(serviceMap) } val params = Arguments.createMap().apply { putString("deviceId", gatt.device.address) putArray("services", services) } sendEvent("servicesDiscovered", params) // --- Queue all mapped characteristics for notification and/or read --- charActionQueue.clear() serviceCharacteristicMap.forEach { (serviceUuid, charUuids) -> val service = gatt.getService(serviceUuid) charUuids.forEach { charUuid -> service?.getCharacteristic(charUuid)?.let { characteristic -> val doNotify = characteristic.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0 // Always set readPending true so we always attempt a read after notification charActionQueue.add(CharAction(characteristic, notifyPending = doNotify, readPending = true)) } } } gattForNotification = gatt processNextCharAction(gatt) pollingGatt = gatt // <-- Fix: ensure pollingGatt is set startSequentialPolling() } else { Log.e("SdkBleModule", "Service discovery failed with status: $status") } } override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) { Log.d("SdkBleModule", "onDescriptorWrite: ${descriptor.characteristic.uuid}, status=$status") val action = charActionQueue.firstOrNull { it.characteristic == descriptor.characteristic } if (status == BluetoothGatt.GATT_SUCCESS && action != null) { action.notifyPending = false Log.d("SdkBleModule", "Notification enabled for ${descriptor.characteristic.uuid}") if (!action.readAttempted) { action.readAttempted = true // Add small delay before reading to ensure notification is fully registered Handler(Looper.getMainLooper()).postDelayed({ gatt.readCharacteristic(descriptor.characteristic) }, 100) // onCharacteristicRead will continue return } } // Add small delay before processing next action to ensure proper notification setup Handler(Looper.getMainLooper()).postDelayed({ processNextCharAction(gatt) }, 150) } override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { Log.d("SdkBleModule", "🔶 onCharacteristicWrite: ${characteristic.uuid}, status=$status, authPending=$authenticationPending, serialPending=$serialNumberPending, deviceTypePending=$deviceTypePending") if (status != BluetoothGatt.GATT_SUCCESS) { Log.d("SdkBleModule", "❌ onCharacteristicWrite error: status=$status") return } // Check if this is the authentication characteristic if (authenticationPending && characteristic.uuid == UUID.fromString("0000ff11-0000-1000-8000-00805f9b34fb")) { Log.d("SdkBleModule", "✅ Authentication command written successfully to FF11") // The response will come through onCharacteristicChanged as a notification } // Check if this is the serial number request characteristic if (serialNumberPending && characteristic.uuid == UUID.fromString("0000ff11-0000-1000-8000-00805f9b34fb")) { Log.d("SdkBleModule", "✅ Serial number request written successfully to FF11") // The response will come through onCharacteristicChanged as a notification } // Check if this is the device type request characteristic if (deviceTypePending && characteristic.uuid == UUID.fromString("0000ff11-0000-1000-8000-00805f9b34fb")) { Log.d("SdkBleModule", "✅ Device type request written successfully to FF11") // The response will come through onCharacteristicChanged as a notification } } override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { // Handle notification data - process immediately without blocking val charUuid = characteristic.uuid.toString().lowercase(Locale.ROOT) val timestamp = System.currentTimeMillis() // Create a copy of the data immediately to avoid race conditions val dataCopy = characteristic.value?.copyOf() ?: ByteArray(0) val charType = when { charUuid.contains("ff02") -> "FF02" charUuid.contains("ff11") -> "FF11" charUuid.contains("ff21") -> "FF21" charUuid.contains("ff31") -> "FF31" charUuid.contains("ff41") -> "FF41" else -> "OTHER" } Log.d("SdkBleModule", ">>> onCharacteristicChanged [${timestamp}] [$charType]: ${characteristic.uuid} - ${dataCopy.size} bytes") // Handle authentication response first if (authenticationPending && charUuid.contains("ff11")) { Log.d("SdkBleModule", "Received authentication response: ${dataCopy.joinToString("-") { String.format("%02X", it) }}") // Check for success response B000 (or just B0 00) if (dataCopy.size >= 2 && dataCopy[0] == 0xB0.toByte() && dataCopy[1] == 0x00.toByte()) { Log.d("SdkBleModule", "✅ Authentication successful (B000)") authenticationPending = false // Send authentication success event val value = android.util.Base64.encodeToString(dataCopy, android.util.Base64.DEFAULT) val hex = dataCopy.joinToString("-") { String.format("%02X", it) } val authParams = Arguments.createMap().apply { putString("deviceId", gatt.device.address) putString("serviceUuid", characteristic.service.uuid.toString()) putString("characteristicUuid", characteristic.uuid.toString()) putString("value", value) putString("hex", hex) putDouble("timestamp", timestamp.toDouble()) putString("type", "FF11") putBoolean("authenticated", true) } sendEvent("characteristicChanged", authParams) // Now request serial number (add small delay to ensure device is ready) Handler(Looper.getMainLooper()).postDelayed({ requestSerialNumber(gatt, characteristic) }, 100) } else { Log.d("SdkBleModule", "❌ Authentication failed - unexpected response: ${dataCopy.joinToString("-") { String.format("%02X", it) }}") authenticationPending = false authenticationCharacteristic = null authenticationGatt = null } return // Don't process further for authentication responses } // Handle serial number response if (serialNumberPending && charUuid.contains("ff11")) { Log.d("SdkBleModule", "Received serial number response: ${dataCopy.joinToString("-") { String.format("%02X", it) }}") // Check for serial number response B1XX where XX is the length if (dataCopy.size >= 2 && dataCopy[0] == 0xB1.toByte()) { val dataLength = dataCopy[1].toInt() and 0xFF Log.d("SdkBleModule", "✅ Serial number response received, length: $dataLength") // Extract serial number from response (skip B1 and length byte) val serialNumberBytes = if (dataCopy.size >= 2 + dataLength) { dataCopy.sliceArray(2 until (2 + dataLength)) } else { dataCopy.sliceArray(2 until dataCopy.size) } val serialNumber = serialNumberBytes.toString(Charsets.UTF_8) Log.d("SdkBleModule", "Serial Number: $serialNumber") serialNumberPending = false authenticationCharacteristic = null authenticationGatt = null // Send serial number event val value = android.util.Base64.encodeToString(dataCopy, android.util.Base64.DEFAULT) val hex = dataCopy.joinToString("-") { String.format("%02X", it) } val serialParams = Arguments.createMap().apply { putString("deviceId", gatt.device.address) putString("serviceUuid", characteristic.service.uuid.toString()) putString("characteristicUuid", characteristic.uuid.toString()) putString("value", value) putString("hex", hex) putDouble("timestamp", timestamp.toDouble()) putString("type", "FF11") putString("serialNumber", serialNumber) putInt("dataLength", dataLength) } sendEvent("characteristicChanged", serialParams) // Now request device type (add small delay to ensure device is ready) Handler(Looper.getMainLooper()).postDelayed({ requestDeviceType(gatt, characteristic) }, 100) } else { Log.d("SdkBleModule", "❌ Serial number request failed - unexpected response: ${dataCopy.joinToString("-") { String.format("%02X", it) }}") serialNumberPending = false authenticationCharacteristic = null authenticationGatt = null } return // Don't process further for serial number responses } // Handle device type response if (deviceTypePending && charUuid.contains("ff11")) { Log.d("SdkBleModule", "Received device type response: ${dataCopy.joinToString("-") { String.format("%02X", it) }}") // Check for device type response B2XX where XX is the length if (dataCopy.size >= 2 && dataCopy[0] == 0xB2.toByte()) { val dataLength = dataCopy[1].toInt() and 0xFF Log.d("SdkBleModule", "✅ Device type response received, length: $dataLength") // Extract device type from response (skip B2 and length byte) val deviceTypeBytes = if (dataCopy.size >= 2 + dataLength) { dataCopy.sliceArray(2 until (2 + dataLength)) } else { dataCopy.sliceArray(2 until dataCopy.size) } val deviceType = deviceTypeBytes.toString(Charsets.UTF_8) Log.d("SdkBleModule", "Device Type: $deviceType") deviceTypePending = false // Send device type event val value = android.util.Base64.encodeToString(dataCopy, android.util.Base64.DEFAULT) val hex = dataCopy.joinToString("-") { String.format("%02X", it) } val deviceTypeParams = Arguments.createMap().apply { putString("deviceId", gatt.device.address) putString("serviceUuid", characteristic.service.uuid.toString()) putString("characteristicUuid", characteristic.uuid.toString()) putString("value", value) putString("hex", hex) putDouble("timestamp", timestamp.toDouble()) putString("type", "FF11") putString("deviceType", deviceType) putInt("dataLength", dataLength) } sendEvent("characteristicChanged", deviceTypeParams) // Now send role command (add small delay to ensure device is ready) Handler(Looper.getMainLooper()).postDelayed({ sendRoleCommand(gatt) // Clean up authentication references after role command is sent authenticationCharacteristic = null authenticationGatt = null }, 100) } else { Log.d("SdkBleModule", "❌ Device type request failed - unexpected response: ${dataCopy.joinToString("-") { String.format("%02X", it) }}") deviceTypePending = false authenticationCharacteristic = null authenticationGatt = null } return // Don't process further for device type responses } // Handle device capabilities response (B3) if (charUuid.contains("ff11") && dataCopy.size >= 3 && dataCopy[0] == 0xB3.toByte()) { Log.d("SdkBleModule", "Received device capabilities response: ${dataCopy.joinToString("-") { String.format("%02X", it) }}") val dataLength = dataCopy[1].toInt() and 0xFF val capabilitiesByte = dataCopy[2].toInt() and 0xFF // Parse capabilities bitmap (bits from right to left) val capDevInfo = (capabilitiesByte and 0x01) != 0 // Bit 0 val capAlert = (capabilitiesByte and 0x02) != 0 // Bit 1 val capTelem = (capabilitiesByte and 0x04) != 0 // Bit 2 val capInfusion = (capabilitiesByte and 0x08) != 0 // Bit 3 val capCtrl = (capabilitiesByte and 0x10) != 0 // Bit 4 val capDfu = (capabilitiesByte and 0x20) != 0 // Bit 5 Log.d("SdkBleModule", "Device Capabilities: DevInfo=$capDevInfo, Alert=$capAlert, Telem=$capTelem, Infusion=$capInfusion, Ctrl=$capCtrl, DFU=$capDfu") // Send capabilities event val value = android.util.Base64.encodeToString(dataCopy, android.util.Base64.DEFAULT) val hex = dataCopy.joinToString("-") { String.format("%02X", it) } val capabilitiesParams = Arguments.createMap().apply { putString("deviceId", gatt.device.address) putString("serviceUuid", characteristic.service.uuid.toString()) putString("characteristicUuid", characteristic.uuid.toString()) putString("value", value) putString("hex", hex) putDouble("timestamp", timestamp.toDouble()) putString("type", "FF11") putInt("dataLength", dataLength) putInt("capabilitiesByte", capabilitiesByte) putString("capabilitiesBinary", String.format("%8s", Integer.toBinaryString(capabilitiesByte)).replace(' ', '0')) // Individual capability flags val capabilities = Arguments.createMap().apply { putBoolean("CAP_DEVINFO", capDevInfo) putBoolean("CAP_ALERT", capAlert) putBoolean("CAP_TELEM", capTelem) putBoolean("CAP_INFUSION", capInfusion) putBoolean("CAP_CTRL", capCtrl) putBoolean("CAP_DFU", capDfu) } putMap("capabilities", capabilities) } sendEvent("characteristicChanged", capabilitiesParams) return // Don't process further for capabilities responses } // Process and send event immediately on the callback thread val value = android.util.Base64.encodeToString(dataCopy, android.util.Base64.DEFAULT) val hex = dataCopy.joinToString("-") { String.format("%02X", it) } val params = Arguments.createMap().apply { putString("deviceId", gatt.device.address) putString("serviceUuid", characteristic.service.uuid.toString()) putString("characteristicUuid", characteristic.uuid.toString()) putString("value", value) putString("hex", hex) putDouble("timestamp", timestamp.toDouble()) putString("type", charType) // Add type for easier filtering if (charUuid == "0000ff31-0000-1000-8000-00805f9b34fb" || charUuid == "ff31") { if (dataCopy.size >= 2) { val structId = dataCopy[0].toInt() and 0xFF val totalLen = dataCopy[1].toInt() and 0xFF putInt("struct_id", structId) putInt("total_len", totalLen) // Extract the data portion (skip first 2 bytes) val chunkData = if (dataCopy.size > 2) { dataCopy.sliceArray(2 until dataCopy.size) } else { ByteArray(0) } // Add to buffer val buffer = ff31ChunkBuffers.getOrPut(structId) { ChunkBuffer(structId) } buffer.chunks.add(chunkData) buffer.lastUpdateTime = timestamp // Clean up old buffers cleanupOldChunkBuffers() // Combine all chunks for this struct_id val combinedData = buffer.chunks.fold(ByteArray(0)) { acc, chunk -> acc + chunk } val combinedString = combinedData.toString(Charsets.UTF_8) putString("stringData", combinedString) putString("stringDataHex", combinedString) putInt("chunk_count", buffer.chunks.size) putInt("combined_length", combinedData.size) putString("chunk_marker", String.format("%c", structId)) Log.d("SdkBleModule", "FF31 chunk received: struct_id=$structId (${String.format("%c", structId)}), totalLen=$totalLen, chunkSize=${chunkData.size}, bufferSize=${buffer.chunks.size}, combined='${combinedString}'") } else { // Fallback: convert entire payload to string val strData = dataCopy.toString(Charsets.UTF_8) putString("stringData", strData) putString("stringDataHex", strData) } } else if (charUuid == "0000ff02-0000-1000-8000-00805f9b34fb" || charUuid == "ff02") { val parsed = parseFF02Data(dataCopy) putMap("parsed", parsed) } else if (charUuid == "0000ff21-0000-1000-8000-00805f9b34fb" || charUuid == "ff21") { val parsed = parseBleNotification(dataCopy, charUuid) putMap("parsed", parsed) } else if (charUuid == "0000ff41-0000-1000-8000-00805f9b34fb" || charUuid == "ff41") { val parsed = parseFF41Data(dataCopy) putMap("parsed", parsed) } } // Send event immediately - this is already on the BLE callback thread sendEvent("characteristicChanged", params) Log.d("SdkBleModule", "<<< Notification EVENT SENT [${timestamp}] [$charType] for ${characteristic.uuid}, hex: ${hex.take(50)}...") } override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { Log.d("SdkBleModule", "onCharacteristicRead: ${characteristic.uuid}, status=$status, isFromPolling=${pollingInProgress}") val action = charActionQueue.firstOrNull { it.characteristic == characteristic } if (action != null) { action.readPending = false } if (status == BluetoothGatt.GATT_SUCCESS) { // Only send event for initial setup reads when not polling // During polling, notifications from onCharacteristicChanged will be used instead if (!pollingInProgress) { val value = android.util.Base64.encodeToString(characteristic.value, android.util.Base64.DEFAULT) val hex = characteristic.value.joinToString("-") { String.format("%02X", it) } val charUuid = characteristic.uuid.toString().lowercase(Locale.ROOT) val params = Arguments.createMap().apply { putString("deviceId", gatt.device.address) putString("serviceUuid", characteristic.service.uuid.toString()) putString("characteristicUuid", characteristic.uuid.toString()) putString("value", value) putString("hex", hex) if (charUuid == "0000ff31-0000-1000-8000-00805f9b34fb" || charUuid == "ff31") { if (characteristic.value.size >= 2) { val structId = characteristic.value[0].toInt() and 0xFF val totalLen = characteristic.value[1].toInt() and 0xFF if (characteristic.value.size >= 2 + totalLen) { val strBytes = characteristic.value.sliceArray(2 until (2 + totalLen)) val strData = strBytes.toString(Charsets.UTF_8) putInt("struct_id", structId) putInt("total_len", totalLen) putString("stringData", strData) putString("stringDataHex", strData) } } } else if (charUuid == "0000ff02-0000-1000-8000-00805f9b34fb" || charUuid == "ff02") { val parsed = parseFF02Data(characteristic.value) putMap("parsed", parsed) } else if (charUuid == "0000ff11-0000-1000-8000-00805f9b34fb" || charUuid == "ff11") { // Authentication characteristic - just include the raw data putString("type", "FF11") } else if (charUuid == "0000ff21-0000-1000-8000-00805f9b34fb" || charUuid == "ff21") { val parsed = parseBleNotification(characteristic.value, charUuid) putMap("parsed", parsed) } else if (charUuid == "0000ff41-0000-1000-8000-00805f9b34fb" || charUuid == "ff41") { val parsed = parseFF41Data(characteristic.value) putMap("parsed", parsed) } } sendEvent("characteristicChanged", params) Log.d("SdkBleModule", "Initial read data sent for ${characteristic.uuid}") } } processNextCharAction(gatt) // --- Sequential polling: trigger next read --- if (pollingActive && gatt == pollingGatt && pollingInProgress) { pollNextCharacteristic() } } private fun processNextCharAction(gatt: BluetoothGatt) { Log.d("SdkBleModule", "processNextCharAction: queue size=${charActionQueue.size}") while (charActionQueue.isNotEmpty()) { val action = charActionQueue.first() val charUuid = action.characteristic.uuid.toString().lowercase(Locale.ROOT) val charType = when { charUuid.contains("ff02") -> "FF02" charUuid.contains("ff11") -> "FF11" charUuid.contains("ff21") -> "FF21" charUuid.contains("ff31") -> "FF31" charUuid.contains("ff41") -> "FF41" else -> "OTHER" } if (action.notifyPending) { Log.d("SdkBleModule", "Enabling notification for [$charType] ${action.characteristic.uuid}") val notifySuccess = gatt.setCharacteristicNotification(action.characteristic, true) Log.d("SdkBleModule", "setCharacteristicNotification result: $notifySuccess for [$charType]") val descriptor = action.characteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")) if (descriptor != null) { descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE val writeSuccess = gatt.writeDescriptor(descriptor) Log.d("SdkBleModule", "writeDescriptor result: $writeSuccess for [$charType]") // onDescriptorWrite will handle read return } else { Log.w("SdkBleModule", "No descriptor found for [$charType] ${action.characteristic.uuid}") action.notifyPending = false } } if (!action.readAttempted) { action.readAttempted = true Log.d("SdkBleModule", "Reading characteristic [$charType] ${action.characteristic.uuid}") gatt.readCharacteristic(action.characteristic) // onCharacteristicRead will continue return } // If both actions are done, remove and continue Log.d("SdkBleModule", "Completed actions for [$charType] ${action.characteristic.uuid}") charActionQueue.removeAt(0) } Log.d("SdkBleModule", "All notifications enabled and all mapped characteristics read") // Start authentication if FF11 characteristic is available and not already authenticated if (!authenticationPending) { performAuthentication(gatt) } } private fun performAuthentication(gatt: BluetoothGatt) { val service = gatt.getService(UUID.fromString("0000ff10-0000-1000-8000-00805f9b34fb")) if (service == null) { Log.d("SdkBleModule", "FF10 service not found, skipping authentication") return } val characteristic = service.getCharacteristic(UUID.fromString("0000ff11-0000-1000-8000-00805f9b34fb")) if (characteristic == null) { Log.d("SdkBleModule", "FF11 characteristic not found, skipping authentication") return } authenticationPending = true authenticationCharacteristic = characteristic authenticationGatt = gatt // Construct command: A006414243313233 // A0 = authentication prefix, 06 = data length, 414243313233 = "ABC123" in hex val authCommand = byteArrayOf(0xA0.toByte(), 0x06, 0x41, 0x42, 0x43, 0x31, 0x32, 0x33) Log.d("SdkBleModule", "Writing authentication command to FF11: ${authCommand.joinToString("") { String.format("%02X", it) }}") characteristic.value = authCommand characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT gatt.writeCharacteristic(characteristic) } private fun requestSerialNumber(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { Log.d("SdkBleModule", "🔵 Starting serial number request...") serialNumberPending = true // Construct command: A100 // A1 = serial number request prefix, 00 = data length (no additional data) val serialCommand = byteArrayOf(0xA1.toByte(), 0x00) Log.d("SdkBleModule", "🔵 Writing serial number request to FF11: ${serialCommand.joinToString("") { String.format("%02X", it) }}") characteristic.value = serialCommand characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT val writeResult = gatt.writeCharacteristic(characteristic) Log.d("SdkBleModule", "🔵 Serial number write result: $writeResult") } private fun requestDeviceType(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { Log.d("SdkBleModule", "🟣 Starting device type request...") deviceTypePending = true // Construct command: A200 // A2 = device type request prefix, 00 = data length (no additional data) val deviceTypeCommand = byteArrayOf(0xA2.toByte(), 0x00) Log.d("SdkBleModule", "🟣 Writing device type request to FF11: ${deviceTypeCommand.joinToString("") { String.format("%02X", it) }}") characteristic.value = deviceTypeCommand characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT val writeResult = gatt.writeCharacteristic(characteristic) Log.d("SdkBleModule", "🟣 Device type write result: $writeResult") } private fun startPolling(gatt: BluetoothGatt) { pollingGatt = gatt pollingActive = true pollHandler.post(pollRunnable) } private fun stopPolling() { pollingActive = false pollHandler.removeCallbacks(pollRunnable) pollingGatt = null pollingQueue.clear() pollingInProgress = false Log.d("SdkBleModule", "Polling stopped and state cleared") } private val pollRunnable = object : Runnable { override fun run() { if (!pollingActive || pollingGatt == null) return serviceCharacteristicMap.forEach { (serviceUuid, charUuids) -> val service = pollingGatt?.getService(serviceUuid) if (service == null) { Log.w("SdkBleModule", "Polling: Service $serviceUuid not found") } charUuids.forEach { charUuid -> val characteristic = service?.getCharacteristic(charUuid) if (characteristic == null) { Log.w("SdkBleModule", "Polling: Characteristic $charUuid not found in service $serviceUuid") } else { val result = pollingGatt?.readCharacteristic(characteristic) Log.d("SdkBleModule", "Polling: Read attempted for $charUuid in $serviceUuid, result=$result") } } } pollHandler.postDelayed(this, pollIntervalMs) } } private fun startSequentialPolling() { pollingActive = true pollingInProgress = false Log.d("SdkBleModule", "startSequentialPolling: pollingActive=$pollingActive, pollingGatt=${pollingGatt != null}") // Delay the start of polling to allow initial notifications to flow freely pollHandler.postDelayed(pollSequentialRunnable, 3000) // Wait 3 seconds before starting polling } private val pollSequentialRunnable = object : Runnable { override fun run() { Log.d("SdkBleModule", "pollSequentialRunnable: pollingActive=$pollingActive, pollingGatt=${pollingGatt != null}, pollingInProgress=$pollingInProgress") if (!pollingActive || pollingGatt == null) return if (!pollingInProgress) { // Fill the queue with all service/char pairs pollingQueue.clear() serviceCharacteristicMap.forEach { (serviceUuid, charUuids) -> charUuids.forEach { charUuid -> pollingQueue.add(serviceUuid to charUuid) } } pollingInProgress = true pollNextCharacteristic() } } } private fun pollNextCharacteristic() { Log.d("SdkBleModule", "pollNextCharacteristic: pollingActive=$pollingActive, pollingGatt=${pollingGatt != null}, queueSize=${pollingQueue.size}") if (!pollingActive || pollingGatt == null) return if (pollingQueue.isEmpty()) { pollingInProgress = false pollHandler.postDelayed(pollSequentialRunnable, pollIntervalMs) return } val (serviceUuid, charUuid) = pollingQueue.removeAt(0) val service = pollingGatt?.getService(serviceUuid) if (service == null) { Log.w("SdkBleModule", "Polling: Service $serviceUuid not found") pollNextCharacteristic() return } val characteristic = service.getCharacteristic(charUuid) if (characteristic == null) { Log.w("SdkBleModule", "Polling: Characteristic $charUuid not found in service $serviceUuid") pollNextCharacteristic() return } val result = pollingGatt?.readCharacteristic(characteristic) Log.d("SdkBleModule", "Polling: Read attempted for $charUuid in $serviceUuid, result=$result") // The next pollNextCharacteristic will be triggered from onCharacteristicRead } } // Required event emitter methods for React Native override fun addListener(eventName: String) { // No-op: Required for React Native event emitter interface } override fun removeListeners(count: Double) { // No-op: Required for React Native event emitter interface } private fun sendEvent(eventName: String, params: WritableMap?) { reactContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(eventName, params) } companion object { const val NAME = "SdkBle" } }