-
Notifications
You must be signed in to change notification settings - Fork 192
Closed
Labels
triageNeeds to be reviewed and assignedNeeds to be reviewed and assigned
Description
Hi,
I currently trying to integrate the gopro ble apis into my android app.
The connection in my code is successfully established and I can get successful responses on get (query) commands.
The issue I have is that no matter which set (control) commands I test the gopro always rejects it.
Based on the documentation I also tried claiming control of the camera (also in documenation) but that also fails.
I have attached my log(code will be put below)
Is my code snippet correct ?
@SuppressLint("MissingPermission")
@OptIn(ExperimentalUnsignedTypes::class)
class GoProBleManager(private val context: Context) {
companion object {
private const val TAG = "GoProBleManager"
// Service UUID
val GP_SERVICE_UUID: UUID = UUID.fromString("0000fea6-0000-1000-8000-00805f9b34fb")
// Command UUIDs (For Actions like Set Time)
val COMMAND_REQ_UUID: UUID = UUID.fromString("b5f90072-aa8d-11e3-9046-0002a5d5c51b")
val COMMAND_RSP_UUID: UUID = UUID.fromString("b5f90073-aa8d-11e3-9046-0002a5d5c51b")
// Query UUIDs (For asking "Are you ready?")
val QUERY_REQ_UUID: UUID = UUID.fromString("b5f90076-aa8d-11e3-9046-0002a5d5c51b")
val QUERY_RSP_UUID: UUID = UUID.fromString("b5f90077-aa8d-11e3-9046-0002a5d5c51b")
// Setup UUIDs
val WIFI_AP_PASSWORD_UUID: UUID = UUID.fromString("b5f90003-aa8d-11e3-9046-0002a5d5c51b")
val CCCD_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
}
private val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
private val adapter: BluetoothAdapter? = bluetoothManager.adapter
// Channels for async callbacks
private val descriptorWriteChannel = Channel<Int>(Channel.UNLIMITED)
private val responseChannel = Channel<ByteArray>(Channel.UNLIMITED)
// We track the Connection State separately
private val connectionStateChannel = Channel<Int>(Channel.CONFLATED)
suspend fun connectAndSetTime(onStatus: (String) -> Unit): Boolean = withContext(Dispatchers.IO) {
if (adapter == null || !adapter.isEnabled) return@withContext false
var gatt: BluetoothGatt? = null
try {
// 1. CONNECT
onStatus("Scanning...")
val device = scanForGoPro() ?: return@withContext false
onStatus("Connecting...")
gatt = connectSafe(device)
// 2. REQUEST HIGH PRIORITY (Fixes many "Busy" issues on Android)
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
// 3. PAIRING (Magic Read)
onStatus("Pairing...")
gatt.discoverServices()
delay(1500)
gatt.getService(GP_SERVICE_UUID)?.getCharacteristic(WIFI_AP_PASSWORD_UUID)?.let {
gatt.readCharacteristic(it)
}
delay(500)
// 4. SUBSCRIBE TO EVERYTHING
onStatus("Subscribing...")
enableAllNotificationsSequential(gatt)
delay(500)
// 5. DEBUG: CHECK IF CAMERA IS READY (Status 8)
// This will tell us if the camera is busy encoding, in a menu, etc.
onStatus("Checking Status...")
val isReady = waitForSystemReady(gatt) { log ->
Log.d(TAG, log)
if(log.contains("Busy")) onStatus("Camera Busy... Waiting")
}
if (!isReady) {
onStatus("Camera is BUSY (In Menu/Encoding?)")
Log.e(TAG, "System Ready (Status 8) returned FALSE or TIMEOUT")
// We continue anyway to try, but we know it will likely fail
}
// 6. SET TIME
onStatus("Setting Time...")
val timeCmd = getSetDateTimeCommand()
val timeResult = sendCommand(gatt, timeCmd, COMMAND_REQ_UUID)
if (timeResult) {
onStatus("Success!")
return@withContext true
}
onStatus("Failed (Busy)")
return@withContext false
} catch (e: Exception) {
Log.e(TAG, "Fatal Error", e)
return@withContext false
} finally {
Log.d(TAG, "Disconnecting...")
try { gatt?.disconnect(); gatt?.close() } catch (_: Exception) {}
}
}
// =========================================================================================
// QUERY LOGIC (The "Is It Ready?" Check)
// =========================================================================================
private suspend fun waitForSystemReady(gatt: BluetoothGatt, onLog: (String) -> Unit): Boolean {
// We poll status ID 8 (System Ready)
// Packet: Length 02, Command 12 (Get Status), Param 08 (System Ready ID)
val query = ubyteArrayOf(0x02U, 0x12U, 0x08U)
for (i in 1..5) {
while(responseChannel.tryReceive().isSuccess) {} // Flush old data
Log.d(TAG, "TX QUERY -> ${query.toHexString()}")
writeSafe(gatt, QUERY_REQ_UUID, query.toByteArray())
try {
withTimeout(2000) {
val rsp = responseChannel.receive() // Wait for notification on QUERY_RSP_UUID
Log.d(TAG, "RX QUERY <- ${rsp.joinToString(":") { "%02X".format(it) }}")
// Response Format: Length, ID(12), Error(00), ValLen(01), Value(01=Ready, 00=Busy)
// Example Ready: 05:13:00:01:01 (Note: ID 12 response is usually 13)
if (rsp.size >= 5) {
val value = rsp[4]
if (value == 0x01.toByte()) {
onLog("System is READY (Value 1)")
return@withTimeout true
} else {
onLog("System is BUSY (Value 0)")
}
}
}
} catch (e: Exception) {
Log.w(TAG, "Query Timeout")
}
delay(1000)
}
return false
}
// =========================================================================================
// BLE HELPERS
// =========================================================================================
private suspend fun sendCommand(gatt: BluetoothGatt, data: UByteArray, uuid: UUID): Boolean {
while(responseChannel.tryReceive().isSuccess) {}
Log.d(TAG, "TX CMD -> ${data.toHexString()}")
writeSafe(gatt, uuid, data.toByteArray())
return try {
withTimeout(4000) {
val response = responseChannel.receive()
Log.d(TAG, "RX CMD <- ${response.joinToString(":") { "%02X".format(it) }}")
// Check Status Byte (usually index 2 for commands)
if (response.size > 2 && response[2] == 0x00.toByte()) return@withTimeout true
false
}
} catch (e: Exception) {
Log.e(TAG, "Command Timeout")
false
}
}
private fun writeSafe(gatt: BluetoothGatt, uuid: UUID, value: ByteArray) {
val char = gatt.getService(GP_SERVICE_UUID)?.getCharacteristic(uuid) ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
gatt.writeCharacteristic(char, value, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
} else {
char.value = value
char.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
gatt.writeCharacteristic(char)
}
}
private suspend fun enableAllNotificationsSequential(gatt: BluetoothGatt) {
val service = gatt.getService(GP_SERVICE_UUID) ?: return
for (char in service.characteristics) {
if ((char.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
enableNotificationSafe(gatt, char)
}
}
}
private suspend fun enableNotificationSafe(gatt: BluetoothGatt, char: BluetoothGattCharacteristic) = suspendCancellableCoroutine { cont ->
gatt.setCharacteristicNotification(char, true)
val desc = char.getDescriptor(CCCD_UUID)
if (desc == null) { cont.resume(Unit); return@suspendCancellableCoroutine }
desc.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
val job = CoroutineScope(Dispatchers.IO).launch {
try {
withTimeout(2000) {
descriptorWriteChannel.receive()
if (cont.isActive) cont.resume(Unit)
}
} catch (e: Exception) { if (cont.isActive) cont.resume(Unit) }
}
val success = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
gatt.writeDescriptor(desc, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) == BluetoothStatusCodes.SUCCESS
} else {
gatt.writeDescriptor(desc)
}
if (!success) { job.cancel(); cont.resume(Unit) }
}
// =========================================================================================
// CALLBACKS
// =========================================================================================
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
connectionStateChannel.trySend(newState)
}
override fun onDescriptorWrite(gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int) {
descriptorWriteChannel.trySend(status)
}
override fun onCharacteristicChanged(gatt: BluetoothGatt, char: BluetoothGattCharacteristic) {
responseChannel.trySend(char.value)
}
override fun onCharacteristicChanged(gatt: BluetoothGatt, char: BluetoothGattCharacteristic, value: ByteArray) {
responseChannel.trySend(value)
}
}
// =========================================================================================
// UTILS
// =========================================================================================
private suspend fun scanForGoPro(): BluetoothDevice? = suspendCancellableCoroutine { cont ->
val scanner = adapter?.bluetoothLeScanner
val callback = object : android.bluetooth.le.ScanCallback() {
override fun onScanResult(c: Int, r: android.bluetooth.le.ScanResult) {
if (r.device.name?.contains("GoPro") == true) {
scanner?.stopScan(this)
if (cont.isActive) cont.resume(r.device)
}
}
override fun onScanFailed(e: Int) { if (cont.isActive) cont.resume(null) }
}
scanner?.startScan(callback)
}
private suspend fun connectSafe(device: BluetoothDevice): BluetoothGatt = suspendCancellableCoroutine { cont ->
val gatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
} else {
device.connectGatt(context, false, gattCallback)
}
val job = CoroutineScope(Dispatchers.IO).launch {
try {
withTimeout(10000) {
while (true) {
val state = connectionStateChannel.receive()
if (state == BluetoothProfile.STATE_CONNECTED) {
if (cont.isActive) cont.resume(gatt)
break
}
}
}
} catch (e: Exception) { /* timeout */ }
}
}
private fun getSetDateTimeCommand(): UByteArray {
val c = Calendar.getInstance()
val bb = ByteBuffer.allocate(9).order(ByteOrder.BIG_ENDIAN)
bb.put(0x08).put(0x0D.toByte())
bb.putShort(c.get(Calendar.YEAR).toShort())
bb.put((c.get(Calendar.MONTH)+1).toByte()).put(c.get(Calendar.DAY_OF_MONTH).toByte())
bb.put(c.get(Calendar.HOUR_OF_DAY).toByte()).put(c.get(Calendar.MINUTE).toByte()).put(c.get(Calendar.SECOND).toByte())
return bb.array().map { it.toUByte() }.toUByteArray()
}
private fun UByteArray.toHexString(): String = joinToString(":") { "%02X".format(it.toInt()) }
}
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
triageNeeds to be reviewed and assignedNeeds to be reviewed and assigned