diff --git a/app/src/main/java/com/example/sensortestingapp/ConnectionManager.kt b/app/src/main/java/com/example/sensortestingapp/ConnectionManager.kt index b88eae5..ed0027f 100644 --- a/app/src/main/java/com/example/sensortestingapp/ConnectionManager.kt +++ b/app/src/main/java/com/example/sensortestingapp/ConnectionManager.kt @@ -20,9 +20,11 @@ import android.annotation.SuppressLint import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothProfile import android.content.Context import android.util.Log +import java.util.UUID import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentLinkedQueue @@ -51,6 +53,12 @@ data class MtuRequest( ) : BleOperationType() +data class ReadChar( + override val device: BluetoothDevice, + val serviceId: UUID, + val charId: UUID +) : BleOperationType() + data class DiscoverServicesRequest( override val device: BluetoothDevice, ) : BleOperationType() @@ -76,6 +84,22 @@ private fun BluetoothGatt.printGattTable() { } +fun BluetoothGattCharacteristic.isReadable(): Boolean = + containsProperty(BluetoothGattCharacteristic.PROPERTY_READ) + +fun BluetoothGattCharacteristic.isWritable(): Boolean = + containsProperty(BluetoothGattCharacteristic.PROPERTY_WRITE) + +fun BluetoothGattCharacteristic.isWritableWithoutResponse(): Boolean = + containsProperty(BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) + +fun BluetoothGattCharacteristic.containsProperty(property: Int): Boolean { + return properties and property != 0 +} + +fun ByteArray.toHexString(): String = + joinToString(separator = "", prefix = "0x") { String.format("%02X", it) } + @SuppressLint("MissingPermission") object ConnectionManager { @@ -114,6 +138,10 @@ object ConnectionManager { } + fun readChar(device: BluetoothDevice, service: UUID, char: UUID) { + enqueueOperation(ReadChar(device, service, char)) + } + // - Beginning of PRIVATE functions @Synchronized @@ -192,6 +220,16 @@ object ConnectionManager { gatt.discoverServices() } + is ReadChar -> with(operation) { + val characteristic = gatt.getService(serviceId)?.getCharacteristic(charId) + if (characteristic?.isReadable() == true) { + gatt.readCharacteristic(characteristic) + } else { + signalEndOfOperation() + } + + } + is Connect -> { Log.e("ConnectionManager", "Shouldn't get here") @@ -260,6 +298,41 @@ object ConnectionManager { } } + override fun onCharacteristicRead( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + status: Int + ) { + with(characteristic) { + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + Log.i( + "ConnectionManager", + "Read characteristic $uuid (service: ${service.uuid}): ${value.toHexString()}" + ) + } + + BluetoothGatt.GATT_READ_NOT_PERMITTED -> { + Log.e( + "BluetoothGattCallback", + "Read not permitted for $uuid (service: ${service.uuid})!" + ) + } + + else -> { + Log.e( + "BluetoothGattCallback", + "Characteristic read failed for $uuid (service: ${service.uuid}), error: $status" + ) + } + } + } + + if (pendingOperation is ReadChar) { + signalEndOfOperation() + } + } + } diff --git a/app/src/main/java/com/example/sensortestingapp/MainActivity.kt b/app/src/main/java/com/example/sensortestingapp/MainActivity.kt index e81390e..388ebb7 100644 --- a/app/src/main/java/com/example/sensortestingapp/MainActivity.kt +++ b/app/src/main/java/com/example/sensortestingapp/MainActivity.kt @@ -27,6 +27,9 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator import com.example.sensortestingapp.databinding.ActivityMainBinding import com.punchthrough.blestarterappandroid.ble.ConnectionManager +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.util.UUID private const val ENABLE_BLUETOOTH_REQUEST_CODE = 1 @@ -39,6 +42,11 @@ private const val GATT_MAX_MTU_SIZE = 517 // Top level declaration private const val GATT_REQUESTED_MTU_SIZE = GATT_MAX_MTU_SIZE + +private val DEMO_SERVICE_UUID = UUID.fromString("00000000-0001-11E1-9AB4-0002A5D5C51B") +private val DEMO_CHAR_UUID = UUID.fromString("00140000-0001-11E1-AC36-0002A5D5C51B") + + fun Context.hasPermission(permissionType: String): Boolean { return ContextCompat.checkSelfPermission(this, permissionType) == PackageManager.PERMISSION_GRANTED @@ -53,6 +61,32 @@ fun Context.hasRequiredRuntimePermissions(): Boolean { } } +data class DemoPayload(val ts: Int, val pressure: Float, val temperature: Float) + + +fun bytesToUInt16(arr: ByteArray, start: Int): Int { + return ByteBuffer.wrap(arr, start, 2) + .order(ByteOrder.LITTLE_ENDIAN).short.toInt() and 0xFFFF +} + +fun bytesToInt16(arr: ByteArray, start: Int): Short { + return ByteBuffer.wrap(arr, start, 2) + .order(ByteOrder.LITTLE_ENDIAN).short +} + +fun bytesToInt32(arr: ByteArray, start: Int): Int { + return ByteBuffer.wrap(arr, start, 4) + .order(ByteOrder.LITTLE_ENDIAN).int +} + + +fun decodeDemoPayload(bytes: ByteArray): DemoPayload { + val ts = bytesToUInt16(bytes, 0) + val pressure = bytesToInt32(bytes, 2) / 100.0f + val temp = bytesToInt16(bytes, 6) / 10.0f; + return DemoPayload(ts, pressure, temp) +} + @SuppressLint("MissingPermission") class MainActivity : AppCompatActivity() { @@ -101,9 +135,11 @@ class MainActivity : AppCompatActivity() { ConnectionManager.connect(scanResult.device, applicationContext) ConnectionManager.requestMtu(scanResult.device, Int.MAX_VALUE) ConnectionManager.discoverServices(scanResult.device) + ConnectionManager.readChar(scanResult.device, DEMO_SERVICE_UUID, DEMO_CHAR_UUID) } } + override fun onCreate(savedInstanceState: Bundle?) { WindowCompat.setDecorFitsSystemWindows(window, false) super.onCreate(savedInstanceState)