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 6c584e74..2ddb6139 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -707,6 +707,11 @@ class BluetoothMeshService(private val context: Context) { val gossip = com.bitchat.android.services.meshgraph.GossipTLV.encodeNeighbors(directPeers) tlvPayload = tlvPayload + gossip } + // Always update our own node in the mesh graph with the neighbor list we used + try { + com.bitchat.android.services.meshgraph.MeshGraphService.getInstance() + .updateFromAnnouncement(myPeerID, nickname, directPeers, System.currentTimeMillis().toULong()) + } catch (_: Exception) { } } catch (_: Exception) { } val announcePacket = BitchatPacket( @@ -763,6 +768,11 @@ class BluetoothMeshService(private val context: Context) { val gossip = com.bitchat.android.services.meshgraph.GossipTLV.encodeNeighbors(directPeers) tlvPayload = tlvPayload + gossip } + // Always update our own node in the mesh graph with the neighbor list we used + try { + com.bitchat.android.services.meshgraph.MeshGraphService.getInstance() + .updateFromAnnouncement(myPeerID, nickname, directPeers, System.currentTimeMillis().toULong()) + } catch (_: Exception) { } } catch (_: Exception) { } val packet = BitchatPacket( diff --git a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt index 7e0bc729..2ebac4db 100644 --- a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt +++ b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt @@ -240,11 +240,11 @@ class MessageHandler(private val myPeerID: String) { previousPeerID = null ) - // Update mesh graph from gossip neighbors (if present) + // Update mesh graph from gossip neighbors (only if TLV present) try { - val neighbors = com.bitchat.android.services.meshgraph.GossipTLV.decodeNeighborsFromAnnouncementPayload(packet.payload) + val neighborsOrNull = com.bitchat.android.services.meshgraph.GossipTLV.decodeNeighborsFromAnnouncementPayload(packet.payload) com.bitchat.android.services.meshgraph.MeshGraphService.getInstance() - .updateFromAnnouncement(peerID, nickname, neighbors, packet.timestamp) + .updateFromAnnouncement(peerID, nickname, neighborsOrNull, packet.timestamp) } catch (_: Exception) { } Log.d(TAG, "✅ Processed verified TLV announce: stored identity for $peerID") diff --git a/app/src/main/java/com/bitchat/android/services/meshgraph/GossipTLV.kt b/app/src/main/java/com/bitchat/android/services/meshgraph/GossipTLV.kt index e684f215..85ea6c9c 100644 --- a/app/src/main/java/com/bitchat/android/services/meshgraph/GossipTLV.kt +++ b/app/src/main/java/com/bitchat/android/services/meshgraph/GossipTLV.kt @@ -24,9 +24,10 @@ object GossipTLV { } /** - * Scan a TLV-encoded announce payload and extract neighbor peerIDs if present. + * Scan a TLV-encoded announce payload and extract neighbor peerIDs. + * Returns null if the TLV is not present at all; returns an empty list if present with length 0. */ - fun decodeNeighborsFromAnnouncementPayload(payload: ByteArray): List { + fun decodeNeighborsFromAnnouncementPayload(payload: ByteArray): List? { val result = mutableListOf() var offset = 0 while (offset + 2 <= payload.size) { @@ -45,10 +46,11 @@ object GossipTLV { result.add(bytesToPeerIdHex(idBytes)) pos += 8 } - break // only one neighbors TLV expected + return result // present (possibly empty) } } - return result + // Not present + return null } private fun hexStringPeerIdTo8Bytes(hexString: String): ByteArray { @@ -73,4 +75,3 @@ object GossipTLV { return sb.toString() } } - diff --git a/app/src/main/java/com/bitchat/android/services/meshgraph/MeshGraphService.kt b/app/src/main/java/com/bitchat/android/services/meshgraph/MeshGraphService.kt index 7decb692..ceb87a91 100644 --- a/app/src/main/java/com/bitchat/android/services/meshgraph/MeshGraphService.kt +++ b/app/src/main/java/com/bitchat/android/services/meshgraph/MeshGraphService.kt @@ -28,33 +28,40 @@ class MeshGraphService private constructor() { * Update graph from a verified announcement. * Replaces previous neighbors for origin if this is newer (by timestamp). */ - fun updateFromAnnouncement(originPeerID: String, originNickname: String?, neighbors: List, timestamp: ULong) { - // Newer-only replacement per origin - val prevTs = lastUpdate[originPeerID] - if (prevTs != null && prevTs >= timestamp) return - lastUpdate[originPeerID] = timestamp - + fun updateFromAnnouncement(originPeerID: String, originNickname: String?, neighborsOrNull: List?, timestamp: ULong) { synchronized(this) { - // Update nickname + // Always update nickname if provided if (originNickname != null) nicknames[originPeerID] = originNickname + // If no neighbors TLV present, do not modify edges or timestamps + if (neighborsOrNull == null) { + publishSnapshot() + return + } + + // Newer-only replacement per origin (based on TLV-bearing announcements only) + val prevTs = lastUpdate[originPeerID] + if (prevTs != null && prevTs >= timestamp) { + // Older or equal TLV-bearing update: ignore + return + } + lastUpdate[originPeerID] = timestamp + // Remove old symmetric edges contributed by this origin val prevNeighbors = adjacency[originPeerID]?.toSet().orEmpty() prevNeighbors.forEach { n -> adjacency[n]?.remove(originPeerID) } - // Replace origin's adjacency with new set - val newSet = neighbors.distinct().take(10).filter { it != originPeerID }.toMutableSet() + // Replace origin's adjacency with new set (may be empty) + val newSet = neighborsOrNull.distinct().take(10).filter { it != originPeerID }.toMutableSet() adjacency[originPeerID] = newSet - // Ensure nodes exist for neighbors + // Ensure undirected edges newSet.forEach { n -> adjacency.putIfAbsent(n, mutableSetOf()) - // Add symmetric edge for undirected graph visualization adjacency[n]?.add(originPeerID) } - // Compute snapshot publishSnapshot() } } @@ -93,4 +100,3 @@ class MeshGraphService private constructor() { } } } - diff --git a/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt b/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt index 6881494d..3bd0ada3 100644 --- a/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt +++ b/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt @@ -26,6 +26,9 @@ import androidx.compose.ui.draw.rotate import com.bitchat.android.mesh.BluetoothMeshService import com.bitchat.android.services.meshgraph.MeshGraphService import kotlinx.coroutines.launch +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.nativeCanvas @Composable fun MeshTopologySection() { @@ -78,6 +81,22 @@ fun MeshTopologySection() { val pos = positions[node.peerID] ?: androidx.compose.ui.geometry.Offset(cx, cy) drawCircle(color = Color(0xFF00C851), radius = 10f, center = pos) } + + // Draw labels near nodes (nickname or short ID) + val labelColor = colorScheme.onSurface.toArgb() + val textSizePx = 10.sp.toPx() + drawIntoCanvas { canvas -> + val paint = android.graphics.Paint().apply { + isAntiAlias = true + color = labelColor + textSize = textSizePx + } + nodes.forEach { node -> + val pos = positions[node.peerID] ?: androidx.compose.ui.geometry.Offset(cx, cy) + val label = (node.nickname?.takeIf { it.isNotBlank() } ?: node.peerID.take(8)) + canvas.nativeCanvas.drawText(label, pos.x + 12f, pos.y - 12f, paint) + } + } } } // Label list for clarity under the canvas @@ -157,11 +176,6 @@ fun DebugSettingsSheet( .padding(bottom = 24.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - item { - // Mesh topology visualization - MeshTopologySection() - } - item { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Icon(Icons.Filled.BugReport, contentDescription = null, tint = Color(0xFFFF9500)) @@ -195,6 +209,11 @@ fun DebugSettingsSheet( } } + // Mesh topology visualization (moved below verbose logging) + item { + MeshTopologySection() + } + // GATT controls item { Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) {