feat: implement GATT client identity signaling and connection deduplication

Implements a mechanism for GATT clients to signal their Peer ID to the server immediately after connection, allowing for robust deduplication of redundant connections.

Includes:
- New IDENTITY_CHARACTERISTIC_UUID in AppConstants
- Server-side handling of identity writes and deduplication logic
- Client-side active signaling of Peer ID upon service discovery
- Updated BluetoothConnectionTracker to map devices to Peer IDs
- Comprehensive specification document in docs/PEERID_GATT_SERVER_CLIENT_SPEC.md
This commit is contained in:
callebtc
2026-01-16 04:14:23 +07:00
parent e3056d70c0
commit 6c7483956e
6 changed files with 260 additions and 2 deletions
@@ -76,7 +76,7 @@ class BluetoothConnectionManager(
context, connectionScope, connectionTracker, permissionManager, powerManager, componentDelegate, myPeerID
)
private val clientManager = BluetoothGattClientManager(
context, connectionScope, connectionTracker, permissionManager, powerManager, componentDelegate
context, connectionScope, connectionTracker, permissionManager, powerManager, componentDelegate, myPeerID
)
// Service state
@@ -167,6 +167,19 @@ class BluetoothConnectionTracker(
return connectedDevices.containsKey(deviceAddress)
}
/**
* Update device peer ID binding
*/
fun setDevicePeerID(deviceAddress: String, peerID: String) {
connectedDevices[deviceAddress]?.let { deviceConn ->
val updatedConn = deviceConn.copy(peerID = peerID)
connectedDevices[deviceAddress] = updatedConn
// Also update the address map
addressPeerMap[deviceAddress] = peerID
Log.d(TAG, "Bound device $deviceAddress to peerID $peerID")
}
}
/**
* Check if a peer is already connected (by PeerID)
*/
@@ -27,7 +27,8 @@ class BluetoothGattClientManager(
private val connectionTracker: BluetoothConnectionTracker,
private val permissionManager: BluetoothPermissionManager,
private val powerManager: PowerManager,
private val delegate: BluetoothConnectionManagerDelegate?
private val delegate: BluetoothConnectionManagerDelegate?,
private val myPeerID: String
) {
companion object {
@@ -480,6 +481,20 @@ class BluetoothGattClientManager(
connectionTracker.updateDeviceConnection(deviceAddress, updatedConn)
Log.d(TAG, "Client: Updated device connection with characteristic for $deviceAddress")
}
// OPTIONAL: Signal our identity to the server (if they support it)
val identityChar = service.getCharacteristic(AppConstants.Mesh.Gatt.IDENTITY_CHARACTERISTIC_UUID)
if (identityChar != null) {
try {
val idBytes = myPeerID.chunked(2).map { it.toInt(16).toByte() }.toByteArray().take(8).toByteArray()
identityChar.value = idBytes
identityChar.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
gatt.writeCharacteristic(identityChar)
Log.d(TAG, "Client: Signaled identity to server $deviceAddress")
} catch (e: Exception) {
Log.w(TAG, "Client: Failed to write identity: ${e.message}")
}
}
gatt.setCharacteristicNotification(characteristic, true)
val descriptor = characteristic.getDescriptor(AppConstants.Mesh.Gatt.DESCRIPTOR_UUID)
@@ -231,6 +231,42 @@ class BluetoothGattServerManager(
Log.w(TAG, "Server: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}")
}
if (responseNeeded) {
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null)
}
} else if (characteristic.uuid == AppConstants.Mesh.Gatt.IDENTITY_CHARACTERISTIC_UUID) {
val peerID = value.joinToString("") { "%02x".format(it) }
Log.i(TAG, "Server: Received Identity signal from ${device.address}: $peerID")
if (value.size >= 8) {
// 1. Update tracker with the declared identity
connectionTracker.setDevicePeerID(device.address, peerID)
// 2. Check for duplicates (same peerID, different MAC)
// Note: If we just set it above, we look for *others*
val duplicate = connectionTracker.getConnectedDevices().values.firstOrNull {
it.peerID == peerID && it.device.address != device.address
}
if (duplicate != null) {
Log.w(TAG, "Server: Deduplication - Peer $peerID is already connected via ${duplicate.device.address}. Rejecting new connection ${device.address}.")
// Send success response first to be polite before killing the connection?
// Or fail it?
if (responseNeeded) {
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null)
}
// Disconnect the NEW connection (this one)
disconnectDevice(device)
return
} else {
Log.d(TAG, "Server: Identity accepted for $peerID at ${device.address}")
}
} else {
Log.w(TAG, "Server: Invalid Identity length from ${device.address}: ${value.size}")
}
if (responseNeeded) {
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null)
}
@@ -310,6 +346,15 @@ class BluetoothGattServerManager(
val service = BluetoothGattService(AppConstants.Mesh.Gatt.SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY)
service.addCharacteristic(characteristic)
// Create identity characteristic for direct client ID signaling
val identityCharacteristic = BluetoothGattCharacteristic(
AppConstants.Mesh.Gatt.IDENTITY_CHARACTERISTIC_UUID,
BluetoothGattCharacteristic.PROPERTY_WRITE or
BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
BluetoothGattCharacteristic.PERMISSION_WRITE
)
service.addCharacteristic(identityCharacteristic)
gattServer?.addService(service)
@@ -28,6 +28,7 @@ object AppConstants {
object Gatt {
val SERVICE_UUID: UUID = UUID.fromString("F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C")
val CHARACTERISTIC_UUID: UUID = UUID.fromString("A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D")
val IDENTITY_CHARACTERISTIC_UUID: UUID = UUID.fromString("A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5E")
val DESCRIPTOR_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
}
}