Reputation: 1
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