Fix BLE ECDH handshake: wait for CCCD write before sending pubkey
All checks were successful
CI / build (push) Successful in 1m5s

BleRelayManager.kt: Android's gatt.writeCharacteristic() returns false when a
previous GATT operation (CCCD writeDescriptor) is still pending. Split
startEcdhHandshake() into two phases: CCCD setup (with ecdhCccdPending flag)
and sendEcdhHandshake() triggered from onDescriptorWrite callback.

main.cpp: After SET_ID over USB, update BLE advertising scan response
manufacturer data so Android can find the server by its new UUID prefix.
This commit is contained in:
eddy 2026-08-04 00:11:36 +02:00
commit 3be06b7469
2 changed files with 43 additions and 7 deletions

View file

@ -81,6 +81,7 @@ class BleRelayManager(
private var ecdhDone = false private var ecdhDone = false
@Volatile private var ecdhDeferred: CompletableDeferred<Boolean>? = null @Volatile private var ecdhDeferred: CompletableDeferred<Boolean>? = null
private var ecdhNotifyBuf: ByteArray = ByteArray(0) private var ecdhNotifyBuf: ByteArray = ByteArray(0)
private var ecdhCccdPending = false
private class CompletableDeferred<T> { private class CompletableDeferred<T> {
private var result: T? = null private var result: T? = null
@ -314,6 +315,7 @@ class BleRelayManager(
ecdhCharacteristic = null ecdhCharacteristic = null
aesKey = null aesKey = null
ecdhDone = false ecdhDone = false
ecdhCccdPending = false
seq = 1 seq = 1
triedAddresses.clear() triedAddresses.clear()
handler.postDelayed(scanRunnable, RECONNECT_DELAY_MS) handler.postDelayed(scanRunnable, RECONNECT_DELAY_MS)
@ -413,6 +415,19 @@ class BleRelayManager(
Log.d(TAG, "ECDH notify received (${value.size} bytes)") Log.d(TAG, "ECDH notify received (${value.size} bytes)")
} }
} }
@SuppressLint("MissingPermission")
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
if (ecdhCccdPending && descriptor.characteristic.uuid == ECDH_UUID) {
ecdhCccdPending = false
if (status == BluetoothGatt.GATT_SUCCESS) {
sendEcdhHandshake(gatt)
} else {
Log.w(TAG, "ECDH: CCCD write failed (status=$status)")
gatt.disconnect()
}
}
}
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
@ -425,18 +440,30 @@ class BleRelayManager(
return return
} }
val cccd = char.getDescriptor(OTA_CCCD_UUID)
if (cccd == null) {
Log.w(TAG, "ECDH: no CCCD descriptor")
gatt.disconnect()
return
}
if (!gatt.setCharacteristicNotification(char, true)) { if (!gatt.setCharacteristicNotification(char, true)) {
Log.w(TAG, "ECDH: setCharacteristicNotification failed") Log.w(TAG, "ECDH: setCharacteristicNotification failed")
gatt.disconnect() gatt.disconnect()
return return
} }
val cccd = char.getDescriptor(OTA_CCCD_UUID) ecdhCccdPending = true
if (cccd != null) {
cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(cccd) if (!gatt.writeDescriptor(cccd)) {
ecdhCccdPending = false
Log.w(TAG, "ECDH: writeDescriptor for CCCD returned false")
gatt.disconnect()
}
} }
@SuppressLint("MissingPermission")
private fun sendEcdhHandshake(gatt: BluetoothGatt) {
val myPsk = psk ?: run { val myPsk = psk ?: run {
Log.w(TAG, "ECDH: no PSK") Log.w(TAG, "ECDH: no PSK")
gatt.disconnect() gatt.disconnect()
@ -462,9 +489,9 @@ class BleRelayManager(
System.arraycopy(sig, 0, handshake, ECDH_PUBKEY_LEN, 32) System.arraycopy(sig, 0, handshake, ECDH_PUBKEY_LEN, 32)
ecdhDeferred = CompletableDeferred() ecdhDeferred = CompletableDeferred()
char.value = handshake ecdhCharacteristic!!.value = handshake
char.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE ecdhCharacteristic!!.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
if (!gatt.writeCharacteristic(char)) { if (!gatt.writeCharacteristic(ecdhCharacteristic!!)) {
Log.w(TAG, "ECDH: write pubkey failed") Log.w(TAG, "ECDH: write pubkey failed")
gatt.disconnect() gatt.disconnect()
return return

View file

@ -617,6 +617,15 @@ void loop() {
preferences.end(); preferences.end();
uuidProvisioned = true; uuidProvisioned = true;
pUuidChar->setValue(deviceUuid, ESP_UUID_LEN); pUuidChar->setValue(deviceUuid, ESP_UUID_LEN);
uint8_t mfgPayload[6];
mfgPayload[0] = 0xFF;
mfgPayload[1] = 0xFF;
memcpy(mfgPayload + 2, deviceUuid, 4);
NimBLEAdvertisementData scanResp;
scanResp.setCompleteServices(NimBLEUUID(SERVICE_UUID));
scanResp.setManufacturerData(std::string((char*)mfgPayload, 6));
NimBLEDevice::getAdvertising()->setScanResponseData(scanResp);
Serial.println("OK"); Serial.println("OK");
Serial.println("UUID stored in NVS"); Serial.println("UUID stored in NVS");
} else { } else {