Cole Rabe
Cole Rabe

Reputation: 1

Unable to Properly Enable Notifications within Android Studio

I have an object within my Android Studio project that I am writing to handle all bluetooth requests (connection, read, write, notify, etc.) and I am having a very hard time getting the android app to properly notify the arduino ble device. I have confirmed that the arduino code works as expected using Bluetooth LE Lab to enable and subscribe to the characteristic, however I have not had the same success with the android code.

Here is my Bluetooth Object that the app interacts with:

package com.example.ledwallgui.ui.settings

import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothProfile
import android.bluetooth.BluetoothSocket
import android.content.Context
import android.os.Handler
import android.util.Log
import java.io.IOException
import java.nio.ByteBuffer
import java.util.LinkedList
import java.util.Queue
import java.util.UUID
import java.util.concurrent.CopyOnWriteArrayList

object GlobalBluetoothManager {
    private val LED_CONNECTION_SERVER: UUID =
        UUID.fromString("12345678-1234-5678-1234-56789abcdef0") //Server for Modes
    private val LED_BRIGHTNESS_CHARACTERISTIC: UUID =
        UUID.fromString("12345678-1234-5678-1234-56789abcdef1") //Server for Modes
    private val LED_MODE_LIST_UUID: UUID =
        UUID.fromString("12345678-1234-5678-1234-56789abcdef2") //Get List from Arduino
    private val CLIENT_DESCRIPTOR_UUID: UUID =
        UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
    private val PARAMETER_REQUEST_UUID: UUID =
        UUID.fromString("12345678-1234-5678-1234-56789abcdef4") //GET LED Parameter Values
    private val MODE_CONTROL_UUID: UUID =
        UUID.fromString("12345678-1234-5678-1234-56789abcdef5") //Write Current Mode
    var bluetoothSocket: BluetoothSocket? = null
    private var _isConnected: Boolean = false
    private var _deviceAddress: String? = null
    private var _deviceName: String? = null  // Added device name
    private var gatt: BluetoothGatt? = null
    var shouldLoadSavedStatus = false
    private val discoveredUUIDs = CopyOnWriteArrayList<UUID>()
    private val ledModeData = mutableListOf<String>()
    var modeInfoList: MutableList<ModeInfo> = mutableListOf()
    var ledParameters: Map<String, String> = mutableMapOf()

    private val gattOperationQueue: Queue<Runnable> = LinkedList()

    // Public function to get discovered UUIDs
    fun getDiscoveredUUIDs(): List<UUID> {
        return discoveredUUIDs
    }

    // Public getter for isConnected
    var isConnected: Boolean = false
        get() = _isConnected

    private const val TAG = "GlobalBluetoothManager"
    private lateinit var handler: Handler

    // Public getter for deviceAddress
    var deviceAddress: String? = null
        get() = _deviceAddress

    // Public getter for deviceName
    var deviceName: String? = null
        get() = _deviceName

    // Method to update the connection state
    fun setConnectionState(isConnected: Boolean, deviceAddress: String?, deviceName: String?) {
        Log.d(
            "GlobalBluetoothManager",
            "setConnectionState called with isConnected = $isConnected, deviceAddress = $deviceAddress, deviceName = $deviceName"
        )

        _isConnected = isConnected
        _deviceAddress = deviceAddress
        _deviceName = deviceName // Update device name

        // Log the updated state
        Log.d(
            "GlobalBluetoothManager",
            "Connection state updated: isConnected = $_isConnected, deviceAddress = $_deviceAddress, deviceName = $_deviceName"
        )
    }

    @SuppressLint("MissingPermission")
    fun connectToDevice(
        context: Context,
        device: BluetoothDevice,
        handler: Handler,
        callback: (Boolean) -> Unit
    ) {
        Log.d(TAG, "connectToDevice called with device: ${device.name}")
        this.handler = handler

        gatt?.close()
        gatt = null

        // Create a BluetoothGattCallback to handle GATT events
        val gattCallback = object : BluetoothGattCallback() {
            override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
                Log.d(TAG, "onConnectionStateChange called with status: $status, newState: $newState")
                if (newState == BluetoothProfile.STATE_CONNECTED) {
                    Log.i(TAG, "Connected to GATT server.")
                    modeInfoList.clear()
                    setConnectionState(true, device.address, device.name)
//                    gatt?.discoverServices() // Discover services after connection
//                    val characteristic = gatt?.getService(LED_CONNECTION_SERVER)?.getCharacteristic(PARAMETER_REQUEST_UUID)
//                    characteristic?.let {
//                        Log.d(TAG, "Discovered1 characteristic: ${it.uuid}")
//                        enableNotificationsForCharacteristic(gatt, it, true)
//                    }
                    enqueueGattOperation {
                        gatt?.discoverServices()
                    }
                } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                    Log.i(TAG, "Disconnected from GATT server.")
                    setConnectionState(false, null, null)
                    callback(false) // Notify disconnection
                }
            }

            override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
                if (status == BluetoothGatt.GATT_SUCCESS) {
                    Log.i(TAG, "Discovered services:")
                    synchronized(discoveredUUIDs) {
                        discoveredUUIDs.clear()

                        gatt?.services?.forEach { service ->
                            Log.d(TAG, "Service UUID: ${service.uuid}")
                            discoveredUUIDs.add(service.uuid)
                            service.characteristics.forEach { characteristic ->
                                Log.d(TAG, "  Characteristic UUID: ${characteristic.uuid}")
                                discoveredUUIDs.add(characteristic.uuid)

                                if (characteristic.uuid == PARAMETER_REQUEST_UUID) {
                                    enqueueGattOperation {
                                        enableNotificationsForCharacteristic(gatt, characteristic, true)
                                    }
                            } else if (characteristic.uuid == LED_MODE_LIST_UUID) {
                                    enqueueGattOperation {
                                        gatt.readCharacteristic(characteristic)
                                    }
                                }
                            }
                        }
                    }
                    callback(true) // Notify successful service discovery
                } else {
                    Log.w(TAG, "onServicesDiscovered received: $status")
                    callback(false) // Notify service discovery failure
                }
            }

            override fun onCharacteristicRead(
                gatt: BluetoothGatt?,
                characteristic: BluetoothGattCharacteristic?,
                status: Int
            ) {
                Log.d(TAG, "onCharacteristicRead called with status: $status")
                val value = characteristic?.value
                if (characteristic != null) {
                    if (value != null) {
                        Log.i(TAG, "Characteristic read successful: ${characteristic.uuid}, Value (raw bytes): ${value.joinToString(", ") { it.toString() }}")
                    }
                }
                val valueAsString = java.lang.String(value, Charsets.UTF_8)
                Log.i(TAG, "Characteristic value (String): $valueAsString")
                if (characteristic != null) {
                    if (status == BluetoothGatt.GATT_SUCCESS && characteristic.uuid == LED_MODE_LIST_UUID) {
                        Log.d("GlobalBluetoothManager", "Received characteristic read response")
                        val value = characteristic.value
                        val rawData = String(value, Charsets.UTF_8) // Convert bytes to string
                        // Log the raw byte array
                        Log.i(TAG, "Characteristic read successful: ${characteristic.uuid}, Value (raw bytes): ${value.joinToString(", ") { it.toString() }}")
                        val valueAsString = String(value, Charsets.UTF_8)
                        Log.i(TAG, "Characteristic value (String): $valueAsString")

                        if (value.size >= 4) {
                            val intValue = ByteBuffer.wrap(value).int
                            Log.i(TAG, "Characteristic value (Integer): $intValue")
                        }
                        // Log the raw data for debugging
                        Log.d(TAG, "Characteristic ${characteristic.uuid} read with raw data: $rawData")

                        // Split the data by semicolons (;) to separate the modes
                        val modeData = rawData.split(";")

                        // Log the raw modes
                        Log.d(TAG, "Split modes: $modeData")

                        // Parse each mode individually
                        modeData.forEach { mode ->
                            // Skip empty strings if present
                            if (mode.isNotBlank()) {
                                // Extract mode name (everything before the first colon)
                                val modeName = mode.substringBefore(":").trim()

                                // Extract parameters (everything after the colon)
                                val parametersString = mode.substringAfter(":").trim()

                                // Split parameters by comma (,) to get individual parameter values
                                val parameters = parametersString.split(",")
                                    .map { it.trim() }
                                    .filter { it.isNotBlank() }

                                // Generate a description from the parameters (for now, we'll just join them into a string)
                                val description = parameters.joinToString(", ")

                                // Create a ModeInfo object for the parsed data and add it to the list
                                val modeInfo = ModeInfo(modeName, description, parameters)
                                modeInfoList.add(modeInfo)

                                // Log the parsed mode info
                                Log.d(TAG, "Parsed ModeInfo: $modeInfo")
                            }
                        }

                        val modeNames = modeInfoList.map { it.name }
                        Log.d(TAG, "All Mode Names: ${modeNames.joinToString(", ")}")
                    } else {
                        Log.w(TAG, "Failed to read characteristic: ${characteristic?.uuid}, status: $status")
                    }
                }
            }

            override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
                if (status == BluetoothGatt.GATT_SUCCESS) {
                    Log.d(TAG, "Characteristic written successfully: ${characteristic.uuid}")
                } else {
                    Log.e(TAG, "Failed to write characteristic: ${characteristic.uuid}, status: $status")
                }
                processGattOperationQueue()
            }

            override fun onCharacteristicChanged(
                gatt: BluetoothGatt?,
                characteristic: BluetoothGattCharacteristic?
            ) {
                super.onCharacteristicChanged(gatt, characteristic)
                Log.d(TAG, "onCharacteristicChanged called")
                if (characteristic != null) {
                    val response = String(characteristic.value, Charsets.UTF_8)
                    Log.d(
                        "BluetoothGatt",
                        "Notification received for characteristic: ${characteristic.uuid}"
                    )
                    Log.d("BluetoothGatt", "Characteristic value: ${characteristic.getStringValue(0)}")
                    Log.d(TAG, "Received parameter values: $response")
                    if (characteristic.uuid == PARAMETER_REQUEST_UUID) {
                        val response = String(characteristic.value, Charsets.UTF_8)
                        Log.d(TAG, "Received parameter values: $response")

                        // Parse the response into a map of parameters
                        val parametersMap = parseParameterResponse(response)
                        ledParameters = parametersMap

                        // Log the parsed parameters
                        Log.d(TAG, "Parsed LED parameters: $ledParameters")

                        parametersMap.forEach { (key, value) ->
                            Log.d(TAG, "Parameter: $key = $value")
                        }
                    }
                    val updatedValue = characteristic.value?.toString(Charsets.UTF_8) ?: "Unknown"
                    Log.d(TAG, "Characteristic ${characteristic.uuid} updated with value: $updatedValue")
                }
            }
            override fun onDescriptorWrite(
                gatt: BluetoothGatt?,
                descriptor: BluetoothGattDescriptor?,
                status: Int
            ) {
                if (status == BluetoothGatt.GATT_SUCCESS) {
                    Log.d(TAG, "Descriptor ${descriptor?.uuid} written successfully.")
                } else {
                    Log.e(TAG, "Failed to write descriptor: ${descriptor?.uuid}, status: $status")
                }
                processGattOperationQueue()
            }
        }

        // Connect to the GATT server
        // Assuming you have an application context available, replace 'context' with it
//        gatt = device.connectGatt(context, false, gattCallback)
        val newGatt = device.connectGatt(context, false, gattCallback)
        if (newGatt != null) {
            gatt = newGatt
            _deviceName = device.name
            Log.d(TAG, "Device name set to: $_deviceName")
            shouldLoadSavedStatus = true
        } else {
            Log.e(TAG, "Failed to connect to GATT server.")
            callback(false) // Notify failure to connect
        }
    }

    private fun parseParameterResponse(response: String): Map<String, String> {
        val params = mutableMapOf<String, String>()
        val pairs = response.split(",")
        for (pair in pairs) {
            val parts = pair.split(":").map { it.trim() }
            if (parts.size == 2) {
                params[parts[0]] = parts[1]
            } else {
                Log.w(TAG, "Skipping malformed parameter: $pair")
            }
        }
        return params
    }

    private fun enableNotificationsForCharacteristic(
        gatt: BluetoothGatt,
        characteristic: BluetoothGattCharacteristic,
        enabled: Boolean
    ) {
        enqueueGattOperation {
            // Enable notifications locally
            val notificationSet = gatt.setCharacteristicNotification(characteristic, enabled)
            if (notificationSet) {
                Log.d(TAG, "Local notifications enabled for ${characteristic.uuid}")
            } else {
                Log.e(TAG, "Failed to enable local notifications for ${characteristic.uuid}")
            }

            // Get the CCCD descriptor for enabling notifications
            Log.d(TAG, "enableNotificationsForCharacteristic called for characteristic: ${characteristic.uuid}")
            Log.d(TAG, "UUID of the descriptor characteristic: $CLIENT_DESCRIPTOR_UUID")

            val descriptor = characteristic.getDescriptor(CLIENT_DESCRIPTOR_UUID)

            // Set the descriptor value based on the enabled state
            val descriptorValue = if (enabled) BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE else BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
            descriptor.value = descriptorValue
            characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT;

            enqueueGattOperation {
                Log.d(TAG, "Attempting to write descriptor for ${characteristic.uuid}")
                Log.d(TAG, "Descriptor value: $descriptor")
                Log.d(TAG, "Descriptor value: ${descriptor.value?.joinToString(", ") { it.toString(16) }}")

                // Now write the descriptor (without passing descriptor value as a second argument)
                if (gatt.writeDescriptor(descriptor)) {
                    Log.d(TAG, "Successfully wrote descriptor: ${descriptor.uuid}")
                } else {
                    Log.e(TAG, "Failed to initiate descriptor write")
                }
            }
        }
    }



    private fun enqueueGattOperation(runnable: Runnable) {
        Log.d("Queue Manager", "Queuing GATT operation: ${runnable.javaClass.simpleName}")
        gattOperationQueue.offer(runnable)
        processGattOperationQueue()
    }

    private fun processGattOperationQueue() {
        if (!gattOperationQueue.isEmpty()) {
            val operation = gattOperationQueue.poll()
            Log.d("Queue Manager", "Running queued GATT operation: ${operation.javaClass.simpleName}")
            operation.run()
        }
    }


    // GlobalBluetoothManager
    fun sendDataToDevice(
        serviceUUID: UUID,
        characteristicUUID: UUID,
        data: String,
        clearBuffer: Boolean = true,
        appendNullTerminator: Boolean = false
    ) {
        val gatt = gatt // Use existing gatt reference

        if (gatt != null && isConnected) {
            val characteristic = gatt.getService(serviceUUID)?.getCharacteristic(characteristicUUID)
            if (characteristic != null) {
                if (clearBuffer) {
                    Log.d(
                        TAG,
                        "sendDataToDevice: Before clearing buffer - characteristic value: ${
                            characteristic.value?.toString(Charsets.UTF_8)
                        }"
                    )
                    characteristic.setValue(byteArrayOf()) // Correct way to clear buffer

                    Log.d(
                        TAG,
                        "sendDataToDevice: After clearing buffer - characteristic value: ${
                            characteristic.value?.toString(Charsets.UTF_8)
                        }"
                    )
                }

                var message = data
                if (appendNullTerminator) {
                    message += '\u0000' // Add null terminator if needed
                }

                characteristic.setValue(message.toByteArray(Charsets.UTF_8)) // Set the new data
                Log.d(
                    TAG,
                    "sendDataToDevice: After setting value - characteristic value: ${
                        characteristic.value?.toString(Charsets.UTF_8)
                    }"
                )
                enqueueGattOperation {
                    val success = gatt.writeCharacteristic(characteristic)
                    if (success) {
                        Log.d(TAG, "Data sent successfully: $data")
                    } else {
                        Log.w(TAG, "Failed to send data")
                    }
                }
            } else {
                Log.w(TAG, "Characteristic not found for UUID: $characteristicUUID")
            }
        } else {
            Log.w(TAG, "Not connected or GATT is null.")
        }
    }

    // Use case 1: Setting LED Mode
    fun setLedMode(modeName: String) {
        val modeControlUUID = MODE_CONTROL_UUID  // Use the provided MODE_CONTROL_UUID
        val ledServiceUUID = LED_CONNECTION_SERVER  // Use the provided LED_CONNECTION_SERVER UUID
        sendDataToDevice(
            ledServiceUUID,
            modeControlUUID,
            modeName,
            clearBuffer = true,
            appendNullTerminator = false
        )
    }

    // Use case 2: Setting LED Parameters
    fun setLedParameters(primaryColor: String, speed: Int, frequency: Int, div: Int) {
        val parametersData = "primaryColor:$primaryColor,speed:$speed,frequency:$frequency,div:$div"
        val modeControlUUID = MODE_CONTROL_UUID  // Use the provided MODE_CONTROL_UUID
        val ledServiceUUID = LED_CONNECTION_SERVER  // Use the provided LED_CONNECTION_SERVER UUID
        sendDataToDevice(
            ledServiceUUID,
            modeControlUUID,
            parametersData,
            clearBuffer = true,
            appendNullTerminator = true
        )
    }

    // Use case 3: Sending Power State to Arduino
    fun sendPowerStateToArduino(state: Boolean) {
        val message = if (state) "ON" else "OFF"
        val powerStateServiceUUID =
            LED_CONNECTION_SERVER  // Use the provided LED_CONNECTION_SERVER UUID
        val powerStateCharacteristicUUID =
            LED_BRIGHTNESS_CHARACTERISTIC  // Use the provided LED_BRIGHTNESS_CHARACTERISTIC UUID
        sendDataToDevice(
            powerStateServiceUUID,
            powerStateCharacteristicUUID,
            message,
            clearBuffer = true,
            appendNullTerminator = false
        )
    }

    fun getModeInfo(modeName: String): ModeInfo? {
        return modeInfoList.find { it.name == modeName }
    }

    fun requestParameterValues(modeName: String) {
        val gatt = gatt // Use existing gatt reference

        if (gatt != null && isConnected) {
            val characteristic =
                gatt.getService(LED_CONNECTION_SERVER)?.getCharacteristic(PARAMETER_REQUEST_UUID)
            if (characteristic != null) {
//                characteristic.setValue(ByteArray(0)) // Clear the buffer
//                characteristic.value = modeName.toByteArray() // Send mode name as request
                val modeNameBytes = modeName.toByteArray(Charsets.UTF_8) + 0.toByte()
                Log.d(TAG, "Requested parameter values for mode: $modeNameBytes")
                characteristic.value = modeNameBytes
                gattOperationQueue.offer(Runnable { gatt.writeCharacteristic(characteristic) })
                processGattOperationQueue()
                Log.d(TAG, "Requested parameter values for mode: $modeName")
            } else {
                Log.w(TAG, "Parameter request characteristic not found")
            }
        } else {
            Log.w(TAG, "Not connected to device")
        }
    }
    // Method to disconnect from a device
    fun disconnect() {
        Log.d(TAG, "disconnect called")
        try {
            bluetoothSocket?.close()
            gatt?.disconnect()
            gatt?.close()
            gatt = null
            _deviceAddress = null
            _deviceName = null
            _isConnected = false
            modeInfoList.clear()
        } catch (e: IOException) {
            Log.e(TAG, "Error during disconnect", e)
        } finally {
            shouldLoadSavedStatus = false
            Log.d(TAG, "Disconnected and resources cleared")
        }
    }


    data class ModeInfo(
        val name: String,
        val description: String,
        val parameters: List<String> // List of parameters as strings
    )
}

Am I missing something here? I understand that there are a couple of depreciated functions as of API 33 but I am assuming that those functions would not prevent it from working. I have also tried to use the Android Guides for BLE and GATT but those are before API 33 and also have not shown any success for me.

Any help at all would be greatly appreciated as this is preventing me from continuing the rest of the work on the app.

Upvotes: 0

Views: 28

Answers (0)

Related Questions