diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt index dd60ea1b..80966be1 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -41,12 +41,23 @@ class BluetoothConnectionManager( // Delegate for component managers to call back to main manager private val componentDelegate = object : BluetoothConnectionManagerDelegate { override fun onPacketReceived(packet: BitchatPacket, peerID: String, device: BluetoothDevice?) { + device?.let { bluetoothDevice -> + // Get current RSSI for this device and update if available + val currentRSSI = connectionTracker.getBestRSSI(bluetoothDevice.address) + if (currentRSSI != null) { + delegate?.onRSSIUpdated(bluetoothDevice.address, currentRSSI) + } + } delegate?.onPacketReceived(packet, peerID, device) } override fun onDeviceConnected(device: BluetoothDevice) { delegate?.onDeviceConnected(device) } + + override fun onRSSIUpdated(deviceAddress: String, rssi: Int) { + delegate?.onRSSIUpdated(deviceAddress, rssi) + } } private val serverManager = BluetoothGattServerManager( @@ -231,4 +242,5 @@ class BluetoothConnectionManager( interface BluetoothConnectionManagerDelegate { fun onPacketReceived(packet: BitchatPacket, peerID: String, device: BluetoothDevice?) fun onDeviceConnected(device: BluetoothDevice) + fun onRSSIUpdated(deviceAddress: String, rssi: Int) } diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt index aa7c23c2..5e91507e 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt @@ -31,6 +31,9 @@ class BluetoothConnectionTracker( private val subscribedDevices = CopyOnWriteArrayList() val addressPeerMap = ConcurrentHashMap() + // RSSI tracking from scan results (for devices we discover but may connect as servers) + private val scanRSSI = ConcurrentHashMap() + // Connection attempt tracking with automatic cleanup private val pendingConnections = ConcurrentHashMap() @@ -117,6 +120,31 @@ class BluetoothConnectionTracker( return subscribedDevices.toList() } + /** + * Get current RSSI for a device address + */ + fun getDeviceRSSI(deviceAddress: String): Int? { + return connectedDevices[deviceAddress]?.rssi?.takeIf { it != Int.MIN_VALUE } + } + + /** + * Store RSSI from scan results + */ + fun updateScanRSSI(deviceAddress: String, rssi: Int) { + scanRSSI[deviceAddress] = rssi + } + + /** + * Get best available RSSI for a device (connection RSSI preferred, then scan RSSI) + */ + fun getBestRSSI(deviceAddress: String): Int? { + // Prefer connection RSSI if available and valid + connectedDevices[deviceAddress]?.rssi?.takeIf { it != Int.MIN_VALUE }?.let { return it } + + // Fall back to scan RSSI + return scanRSSI[deviceAddress] + } + /** * Add a subscribed device */ @@ -249,6 +277,7 @@ class BluetoothConnectionTracker( subscribedDevices.clear() addressPeerMap.clear() pendingConnections.clear() + scanRSSI.clear() } /** @@ -300,6 +329,11 @@ class BluetoothConnectionTracker( val elapsed = (now - attempt.lastAttempt) / 1000 appendLine(" - $address: ${attempt.attempts} attempts, last ${elapsed}s ago") } + appendLine() + appendLine("Scan RSSI Cache: ${scanRSSI.size}") + scanRSSI.forEach { (address, rssi) -> + appendLine(" - $address: $rssi dBm") + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt index 3b112478..f60c68ce 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.* +import kotlinx.coroutines.Job /** * Manages GATT client operations, scanning, and client-side connections @@ -31,6 +32,9 @@ class BluetoothGattClientManager( // Use exact same UUIDs as iOS version private val SERVICE_UUID = UUID.fromString("F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C") private val CHARACTERISTIC_UUID = UUID.fromString("A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D") + + // RSSI monitoring constants + private const val RSSI_UPDATE_INTERVAL = 5000L // 5 seconds } // Core Bluetooth components @@ -48,6 +52,9 @@ class BluetoothGattClientManager( private var isCurrentlyScanning = false private val scanRateLimit = 5000L // Minimum 5 seconds between scan start attempts + // RSSI monitoring state + private var rssiMonitoringJob: Job? = null + // State management private var isActive = false @@ -78,6 +85,9 @@ class BluetoothGattClientManager( } else { startScanning() } + + // Start RSSI monitoring + startRSSIMonitoring() } return true @@ -91,6 +101,7 @@ class BluetoothGattClientManager( connectionScope.launch { stopScanning() + stopRSSIMonitoring() Log.i(TAG, "GATT client manager stopped") } } @@ -106,6 +117,41 @@ class BluetoothGattClientManager( } } + /** + * Start periodic RSSI monitoring for all client connections + */ + private fun startRSSIMonitoring() { + rssiMonitoringJob?.cancel() + rssiMonitoringJob = connectionScope.launch { + while (isActive) { + try { + // Request RSSI from all client connections + val connectedDevices = connectionTracker.getConnectedDevices() + connectedDevices.values.filter { it.isClient && it.gatt != null }.forEach { deviceConn -> + try { + Log.d(TAG, "Requesting RSSI from ${deviceConn.device.address}") + deviceConn.gatt?.readRemoteRssi() + } catch (e: Exception) { + Log.w(TAG, "Failed to request RSSI from ${deviceConn.device.address}: ${e.message}") + } + } + delay(RSSI_UPDATE_INTERVAL) + } catch (e: Exception) { + Log.w(TAG, "Error in RSSI monitoring: ${e.message}") + delay(RSSI_UPDATE_INTERVAL) + } + } + } + } + + /** + * Stop RSSI monitoring + */ + private fun stopRSSIMonitoring() { + rssiMonitoringJob?.cancel() + rssiMonitoringJob = null + } + /** * Start scanning with rate limiting */ @@ -230,6 +276,9 @@ class BluetoothGattClientManager( return } + // Store RSSI from scan results for later use (especially for server connections) + connectionTracker.updateScanRSSI(deviceAddress, rssi) + // Power-aware RSSI filtering if (rssi < powerManager.getRSSIThreshold()) { Log.d(TAG, "Skipping device $deviceAddress due to weak signal: $rssi < ${powerManager.getRSSIThreshold()}") @@ -381,6 +430,21 @@ class BluetoothGattClientManager( Log.w(TAG, "Client: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}") } } + + override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) { + val deviceAddress = gatt.device.address + if (status == BluetoothGatt.GATT_SUCCESS) { + Log.d(TAG, "Client: RSSI updated for $deviceAddress: $rssi dBm") + + // Update the connection tracker with new RSSI value + connectionTracker.getDeviceConnection(deviceAddress)?.let { deviceConn -> + val updatedConn = deviceConn.copy(rssi = rssi) + connectionTracker.updateDeviceConnection(deviceAddress, updatedConn) + } + } else { + Log.w(TAG, "Client: Failed to read RSSI for $deviceAddress, status: $status") + } + } } try { diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt index c73ea16c..d70f2353 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt @@ -69,7 +69,7 @@ class BluetoothGattServerManager( isActive = true connectionScope.launch { - setupGattServer() + // setupGattServer() delay(300) // Brief delay to ensure GATT server is ready startAdvertising() } @@ -122,8 +122,13 @@ class BluetoothGattServerManager( when (newState) { BluetoothProfile.STATE_CONNECTED -> { Log.i(TAG, "Server: Device connected ${device.address}") + + // Get best available RSSI (scan RSSI for server connections) + val rssi = connectionTracker.getBestRSSI(device.address) ?: Int.MIN_VALUE + val deviceConn = BluetoothConnectionTracker.DeviceConnection( device = device, + rssi = rssi, isClient = false ) connectionTracker.addDeviceConnection(device.address, deviceConn) diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt index 803d65dd..dca98057 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -280,6 +280,13 @@ class BluetoothMeshService(private val context: Context) { sendKeyExchangeToDevice() } } + + override fun onRSSIUpdated(deviceAddress: String, rssi: Int) { + // Find the peer ID for this device address and update RSSI in PeerManager + connectionManager.addressPeerMap[deviceAddress]?.let { peerID -> + peerManager.updatePeerRSSI(peerID, rssi) + } + } } } diff --git a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt index 112c354c..27e6927f 100644 --- a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt @@ -299,7 +299,7 @@ fun PeopleSection( PeerItem( peerID = peerID, displayName = if (peerID == nickname) "You" else (peerNicknames[peerID] ?: peerID), - signalStrength = peerRSSI[peerID] ?: 0, + signalStrength = convertRSSIToSignalStrength(peerRSSI[peerID]), isSelected = peerID == selectedPrivatePeer, isFavorite = isFavorite, hasUnreadDM = hasUnreadPrivateMessages.contains(peerID), @@ -437,3 +437,25 @@ private fun UnreadBadge( } } } + +/** + * Convert RSSI value (dBm) to signal strength percentage (0-100) + * RSSI typically ranges from -30 (excellent) to -100 (very poor) + * Maps to 0-100 scale where: + * - 0-32: No signal (0 bars) + * - 33-65: Weak (1 bar) + * - 66-98: Good (2 bars) + * - 99-100: Excellent (3 bars) + */ +private fun convertRSSIToSignalStrength(rssi: Int?): Int { + if (rssi == null) return 0 + + return when { + rssi >= -40 -> 100 // Excellent signal + rssi >= -55 -> 85 // Very good signal + rssi >= -70 -> 70 // Good signal + rssi >= -85 -> 50 // Fair signal + rssi >= -100 -> 25 // Poor signal + else -> 0 // Very poor or no signal + } +}