From c1e56188d648e6162605c00a55b186d55b6d9a42 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 8 Sep 2025 15:15:54 +0200 Subject: [PATCH] Revert "Mesh gossip (#381)" (#394) This reverts commit 0969c0641eeb272462ef0d5c21f7b0b3cd6b8bea. --- .../mesh/BluetoothConnectionManager.kt | 10 - .../android/mesh/BluetoothMeshService.kt | 74 +------ .../mesh/BluetoothPacketBroadcaster.kt | 40 ---- .../bitchat/android/mesh/MessageHandler.kt | 7 - .../bitchat/android/mesh/PacketProcessor.kt | 4 - .../android/mesh/PacketRelayManager.kt | 41 +--- .../android/protocol/BinaryProtocol.kt | 49 +---- .../android/services/meshgraph/GossipTLV.kt | 77 ------- .../services/meshgraph/MeshGraphService.kt | 102 ---------- .../services/meshgraph/RoutePlanner.kt | 66 ------ .../android/ui/debug/DebugSettingsSheet.kt | 189 +++--------------- 11 files changed, 38 insertions(+), 621 deletions(-) delete mode 100644 app/src/main/java/com/bitchat/android/services/meshgraph/GossipTLV.kt delete mode 100644 app/src/main/java/com/bitchat/android/services/meshgraph/MeshGraphService.kt delete mode 100644 app/src/main/java/com/bitchat/android/services/meshgraph/RoutePlanner.kt 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 23539dcb..0cc7f967 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -247,16 +247,6 @@ class BluetoothConnectionManager( serverManager.getCharacteristic() ) } - - fun sendToPeer(peerID: String, routed: RoutedPacket): Boolean { - if (!isActive) return false - return packetBroadcaster.sendToPeer( - peerID, - routed, - serverManager.getGattServer(), - serverManager.getCharacteristic() - ) - } // Expose role controls for debug UI 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 6e27d6ec..723d603e 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -408,10 +408,6 @@ class BluetoothMeshService(private val context: Context) { override fun relayPacket(routed: RoutedPacket) { connectionManager.broadcastPacket(routed) } - - override fun sendToPeer(peerID: String, routed: RoutedPacket): Boolean { - return connectionManager.sendToPeer(peerID, routed) - } } // BluetoothConnectionManager delegates @@ -692,25 +688,11 @@ class BluetoothMeshService(private val context: Context) { // Create iOS-compatible IdentityAnnouncement with TLV encoding val announcement = IdentityAnnouncement(nickname, staticKey, signingKey) - var tlvPayload = announcement.encode() + val tlvPayload = announcement.encode() if (tlvPayload == null) { Log.e(TAG, "Failed to encode announcement as TLV") return@launch } - - // Append gossip TLV containing up to 10 direct neighbors (compact IDs) - try { - val directPeers = getDirectPeerIDsForGossip() - if (directPeers.isNotEmpty()) { - 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( type = MessageType.ANNOUNCE.value, @@ -753,25 +735,11 @@ class BluetoothMeshService(private val context: Context) { // Create iOS-compatible IdentityAnnouncement with TLV encoding val announcement = IdentityAnnouncement(nickname, staticKey, signingKey) - var tlvPayload = announcement.encode() + val tlvPayload = announcement.encode() if (tlvPayload == null) { Log.e(TAG, "Failed to encode peer announcement as TLV") return } - - // Append gossip TLV containing up to 10 direct neighbors (compact IDs) - try { - val directPeers = getDirectPeerIDsForGossip() - if (directPeers.isNotEmpty()) { - 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( type = MessageType.ANNOUNCE.value, @@ -790,20 +758,6 @@ class BluetoothMeshService(private val context: Context) { Log.d(TAG, "Sent iOS-compatible signed TLV peer announce to $peerID (${tlvPayload.size} bytes)") } - /** - * Collect up to 10 direct neighbors for gossip TLV. - */ - private fun getDirectPeerIDsForGossip(): List { - return try { - // Prefer verified peers that are currently marked as direct - val verified = peerManager.getVerifiedPeers() - val direct = verified.filter { it.value.isDirectConnection }.keys.toList() - direct.take(10) - } catch (_: Exception) { - emptyList() - } - } - /** * Send leave announcement */ @@ -985,37 +939,21 @@ class BluetoothMeshService(private val context: Context) { */ private fun signPacketBeforeBroadcast(packet: BitchatPacket): BitchatPacket { return try { - // Optionally compute and attach a source route for addressed packets - val withRoute = try { - val rec = packet.recipientID - if (rec != null && !rec.contentEquals(SpecialRecipients.BROADCAST)) { - val dest = rec.joinToString("") { b -> "%02x".format(b) } - val path = com.bitchat.android.services.meshgraph.RoutePlanner.shortestPath(myPeerID, dest) - if (path != null && path.size >= 3) { - // Exclude first (sender) and last (recipient); only intermediates - val intermediates = path.subList(1, path.size - 1) - val hopsBytes = intermediates.map { hexStringToByteArray(it) } - Log.d(TAG, "✅ Signed packet type ${packet.type} (route ${hopsBytes.size} hops: $intermediates)") - packet.copy(route = hopsBytes) - } else packet.copy(route = null) - } else packet - } catch (_: Exception) { packet } - // Get the canonical packet data for signing (without signature) - val packetDataForSigning = withRoute.toBinaryDataForSigning() + val packetDataForSigning = packet.toBinaryDataForSigning() if (packetDataForSigning == null) { Log.w(TAG, "Failed to encode packet type ${packet.type} for signing, sending unsigned") - return withRoute + return packet } // Sign the packet data using our signing key val signature = encryptionService.signData(packetDataForSigning) if (signature != null) { Log.d(TAG, "✅ Signed packet type ${packet.type} (signature ${signature.size} bytes)") - withRoute.copy(signature = signature) + packet.copy(signature = signature) } else { Log.w(TAG, "Failed to sign packet type ${packet.type}, sending unsigned") - withRoute + packet } } catch (e: Exception) { Log.w(TAG, "Error signing packet type ${packet.type}: ${e.message}, sending unsigned") diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt index b5c5484f..1fc52bb4 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt @@ -159,46 +159,6 @@ class BluetoothPacketBroadcaster( } } } - - /** - * Targeted send to a specific peer (by peerID) if directly connected. - * Returns true if sent to at least one matching connection. - */ - fun sendToPeer( - targetPeerID: String, - routed: RoutedPacket, - gattServer: BluetoothGattServer?, - characteristic: BluetoothGattCharacteristic? - ): Boolean { - val packet = routed.packet - val data = packet.toBinaryData() ?: return false - val typeName = MessageType.fromValue(packet.type)?.name ?: packet.type.toString() - val senderPeerID = routed.peerID ?: packet.senderID.toHexString() - val incomingAddr = routed.relayAddress - val incomingPeer = incomingAddr?.let { connectionTracker.addressPeerMap[it] } - val senderNick = senderPeerID.let { pid -> nicknameResolver?.invoke(pid) } - - // Try server-side connections first - val targetDevice = connectionTracker.getSubscribedDevices() - .firstOrNull { connectionTracker.addressPeerMap[it.address] == targetPeerID } - if (targetDevice != null) { - if (notifyDevice(targetDevice, data, gattServer, characteristic)) { - logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, targetPeerID, targetDevice.address, packet.ttl) - return true - } - } - - // Try client-side connections next - val targetConn = connectionTracker.getConnectedDevices().values - .firstOrNull { connectionTracker.addressPeerMap[it.device.address] == targetPeerID } - if (targetConn != null) { - if (writeToDeviceConn(targetConn, data)) { - logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, targetPeerID, targetConn.device.address, packet.ttl) - return true - } - } - return false - } /** * Internal broadcast implementation - runs in serialized actor context 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 4a9c13dc..c2faffea 100644 --- a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt +++ b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt @@ -240,13 +240,6 @@ class MessageHandler(private val myPeerID: String) { previousPeerID = null ) - // Update mesh graph from gossip neighbors (only if TLV present) - try { - val neighborsOrNull = com.bitchat.android.services.meshgraph.GossipTLV.decodeNeighborsFromAnnouncementPayload(packet.payload) - com.bitchat.android.services.meshgraph.MeshGraphService.getInstance() - .updateFromAnnouncement(peerID, nickname, neighborsOrNull, packet.timestamp) - } catch (_: Exception) { } - Log.d(TAG, "✅ Processed verified TLV announce: stored identity for $peerID") return isFirstAnnounce } diff --git a/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt b/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt index 315308e2..54ecffbe 100644 --- a/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt +++ b/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt @@ -112,9 +112,6 @@ class PacketProcessor(private val myPeerID: String) { override fun broadcastPacket(routed: RoutedPacket) { delegate?.relayPacket(routed) } - override fun sendToPeer(peerID: String, routed: RoutedPacket): Boolean { - return delegate?.sendToPeer(peerID, routed) ?: false - } } } @@ -313,5 +310,4 @@ interface PacketProcessorDelegate { fun sendAnnouncementToPeer(peerID: String) fun sendCachedMessages(peerID: String) fun relayPacket(routed: RoutedPacket) - fun sendToPeer(peerID: String, routed: RoutedPacket): Boolean } diff --git a/app/src/main/java/com/bitchat/android/mesh/PacketRelayManager.kt b/app/src/main/java/com/bitchat/android/mesh/PacketRelayManager.kt index 246960bf..f292534b 100644 --- a/app/src/main/java/com/bitchat/android/mesh/PacketRelayManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/PacketRelayManager.kt @@ -65,35 +65,9 @@ class PacketRelayManager(private val myPeerID: String) { val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) Log.d(TAG, "Decremented TTL from ${'$'}{packet.ttl} to ${'$'}{relayPacket.ttl}") - // Source-based routing: if route is set and includes us, try targeted next-hop forwarding - val route = relayPacket.route - if (!route.isNullOrEmpty()) { - val myIdBytes = hexStringToPeerBytes(myPeerID) - val index = route.indexOfFirst { it.contentEquals(myIdBytes) } - if (index >= 0) { - val nextHopIdHex: String? = run { - val nextIndex = index + 1 - if (nextIndex < route.size) { - route[nextIndex].toHexString() - } else { - // We are the last intermediate; try final recipient as next hop - relayPacket.recipientID?.toHexString() - } - } - if (nextHopIdHex != null) { - val success = try { delegate?.sendToPeer(nextHopIdHex, RoutedPacket(relayPacket, peerID, routed.relayAddress)) } catch (_: Exception) { false } ?: false - if (success) { - Log.i(TAG, "📦 Source-route relay: ${myPeerID.take(8)} -> ${nextHopIdHex.take(8)} (type ${'$'}{packet.type}, TTL ${'$'}{relayPacket.ttl})") - return - } else { - Log.w(TAG, "Source-route next hop ${nextHopIdHex.take(8)} not directly connected; falling back to broadcast") - } - } - } - } - // Apply relay logic based on packet type and debug switch val shouldRelay = isRelayEnabled() && shouldRelayPacket(relayPacket, peerID) + if (shouldRelay) { relayPacket(RoutedPacket(relayPacket, peerID, routed.relayAddress)) } else { @@ -232,17 +206,4 @@ interface PacketRelayManagerDelegate { // Packet operations fun broadcastPacket(routed: RoutedPacket) - fun sendToPeer(peerID: String, routed: RoutedPacket): Boolean -} - -private fun hexStringToPeerBytes(hex: String): ByteArray { - val result = ByteArray(8) - var idx = 0 - var out = 0 - while (idx + 1 < hex.length && out < 8) { - val b = hex.substring(idx, idx + 2).toIntOrNull(16)?.toByte() ?: 0 - result[out++] = b - idx += 2 - } - return result } diff --git a/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt b/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt index fee7aaad..3dcc47a2 100644 --- a/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt +++ b/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt @@ -57,8 +57,7 @@ data class BitchatPacket( val timestamp: ULong, val payload: ByteArray, var signature: ByteArray? = null, // Changed from val to var for packet signing - var ttl: UByte, - var route: List? = null // Optional source route: ordered list of peerIDs (8 bytes each), not including sender and final recipient + var ttl: UByte ) : Parcelable { constructor( @@ -96,8 +95,7 @@ data class BitchatPacket( timestamp = timestamp, payload = payload, signature = null, // Remove signature for signing - ttl = 0u, // Use fixed TTL=0 for signing to ensure relay compatibility - route = route + ttl = 0u // Use fixed TTL=0 for signing to ensure relay compatibility ) return BinaryProtocol.encode(unsignedPacket) } @@ -149,11 +147,6 @@ data class BitchatPacket( if (!signature.contentEquals(other.signature)) return false } else if (other.signature != null) return false if (ttl != other.ttl) return false - if (route != null || other.route != null) { - val a = route?.map { it.toList() } ?: emptyList() - val b = other.route?.map { it.toList() } ?: emptyList() - if (a != b) return false - } return true } @@ -167,7 +160,6 @@ data class BitchatPacket( result = 31 * result + payload.contentHashCode() result = 31 * result + (signature?.contentHashCode() ?: 0) result = 31 * result + ttl.hashCode() - result = 31 * result + (route?.fold(1) { acc, bytes -> 31 * acc + bytes.contentHashCode() } ?: 0) return result } } @@ -185,7 +177,6 @@ object BinaryProtocol { const val HAS_RECIPIENT: UByte = 0x01u const val HAS_SIGNATURE: UByte = 0x02u const val IS_COMPRESSED: UByte = 0x04u - const val HAS_ROUTE: UByte = 0x08u } fun encode(packet: BitchatPacket): ByteArray? { @@ -224,9 +215,6 @@ object BinaryProtocol { if (isCompressed) { flags = flags or Flags.IS_COMPRESSED } - if (!packet.route.isNullOrEmpty()) { - flags = flags or Flags.HAS_ROUTE - } buffer.put(flags.toByte()) // Payload length (2 bytes, big-endian) - includes original size if compressed @@ -248,14 +236,6 @@ object BinaryProtocol { buffer.put(ByteArray(RECIPIENT_ID_SIZE - recipientBytes.size)) } } - - // Route (optional): 1 byte count + N*8 bytes - packet.route?.let { routeList -> - val cleaned = routeList.map { bytes -> bytes.take(SENDER_ID_SIZE).toByteArray().let { if (it.size < SENDER_ID_SIZE) it + ByteArray(SENDER_ID_SIZE - it.size) else it } } - val count = cleaned.size.coerceAtMost(255) - buffer.put(count.toByte()) - cleaned.take(count).forEach { hop -> buffer.put(hop) } - } // Payload (with original size prepended if compressed) if (isCompressed) { @@ -322,7 +302,6 @@ object BinaryProtocol { val hasRecipient = (flags and Flags.HAS_RECIPIENT) != 0u.toUByte() val hasSignature = (flags and Flags.HAS_SIGNATURE) != 0u.toUByte() val isCompressed = (flags and Flags.IS_COMPRESSED) != 0u.toUByte() - val hasRoute = (flags and Flags.HAS_ROUTE) != 0u.toUByte() // Payload length val payloadLength = buffer.getShort().toUShort() @@ -330,15 +309,6 @@ object BinaryProtocol { // Calculate expected total size var expectedSize = HEADER_SIZE + SENDER_ID_SIZE + payloadLength.toInt() if (hasRecipient) expectedSize += RECIPIENT_ID_SIZE - var routeCount = 0 - if (hasRoute) { - // Peek count (1 byte) without consuming buffer for now - val mark = buffer.position() - if (raw.size >= mark + 1) { - routeCount = raw[mark].toUByte().toInt() - } - expectedSize += 1 + (routeCount * SENDER_ID_SIZE) - } if (hasSignature) expectedSize += SIGNATURE_SIZE if (raw.size < expectedSize) return null @@ -354,18 +324,6 @@ object BinaryProtocol { recipientBytes } else null - // Route (optional) - val route: List? = if (hasRoute) { - val count = buffer.get().toUByte().toInt() - val hops = mutableListOf() - repeat(count) { - val hop = ByteArray(SENDER_ID_SIZE) - buffer.get(hop) - hops.add(hop) - } - hops - } else null - // Payload val payload = if (isCompressed) { // First 2 bytes are original size @@ -399,8 +357,7 @@ object BinaryProtocol { timestamp = timestamp, payload = payload, signature = signature, - ttl = ttl, - route = route + ttl = ttl ) } catch (e: Exception) { 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 deleted file mode 100644 index 85ea6c9c..00000000 --- a/app/src/main/java/com/bitchat/android/services/meshgraph/GossipTLV.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.bitchat.android.services.meshgraph - -import android.util.Log - -/** - * Gossip TLV helpers for embedding direct neighbor peer IDs in ANNOUNCE payloads. - * Uses compact TLV: [type=0x04][len=1 byte][value=N*8 bytes of peerIDs] - */ -object GossipTLV { - // TLV type for a compact list of direct neighbor peerIDs (each 8 bytes) - const val DIRECT_NEIGHBORS_TYPE: UByte = 0x04u - - /** - * Encode up to 10 unique peerIDs (hex string up to 16 chars) as TLV value. - */ - fun encodeNeighbors(peerIDs: List): ByteArray { - val unique = peerIDs.distinct().take(10) - val valueBytes = unique.flatMap { id -> hexStringPeerIdTo8Bytes(id).toList() }.toByteArray() - if (valueBytes.size > 255) { - // Safety check, though 10*8 = 80 bytes, so well under 255 - Log.w("GossipTLV", "Neighbors value exceeds 255, truncating") - } - return byteArrayOf(DIRECT_NEIGHBORS_TYPE.toByte(), valueBytes.size.toByte()) + valueBytes - } - - /** - * 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? { - val result = mutableListOf() - var offset = 0 - while (offset + 2 <= payload.size) { - val type = payload[offset].toUByte() - val len = payload[offset + 1].toUByte().toInt() - offset += 2 - if (offset + len > payload.size) break - val value = payload.sliceArray(offset until offset + len) - offset += len - - if (type == DIRECT_NEIGHBORS_TYPE) { - // Value is N*8 bytes of peer IDs - var pos = 0 - while (pos + 8 <= value.size) { - val idBytes = value.sliceArray(pos until pos + 8) - result.add(bytesToPeerIdHex(idBytes)) - pos += 8 - } - return result // present (possibly empty) - } - } - // Not present - return null - } - - private fun hexStringPeerIdTo8Bytes(hexString: String): ByteArray { - val clean = hexString.lowercase().take(16) - val result = ByteArray(8) { 0 } - var idx = 0 - var out = 0 - while (idx + 1 < clean.length && out < 8) { - val byteStr = clean.substring(idx, idx + 2) - val b = byteStr.toIntOrNull(16)?.toByte() ?: 0 - result[out++] = b - idx += 2 - } - return result - } - - private fun bytesToPeerIdHex(bytes: ByteArray): String { - val sb = StringBuilder() - for (b in bytes.take(8)) { - sb.append(String.format("%02x", b)) - } - 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 deleted file mode 100644 index ceb87a91..00000000 --- a/app/src/main/java/com/bitchat/android/services/meshgraph/MeshGraphService.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.bitchat.android.services.meshgraph - -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import java.util.concurrent.ConcurrentHashMap - -/** - * Maintains an internal undirected graph of the mesh based on gossip. - * Nodes are peers (peerID), edges are direct connections. - */ -class MeshGraphService private constructor() { - data class GraphNode(val peerID: String, val nickname: String?) - data class GraphEdge(val a: String, val b: String) - data class GraphSnapshot(val nodes: List, val edges: List) - - // Map peerID -> nickname (may be null if unknown) - private val nicknames = ConcurrentHashMap() - // Adjacency (undirected): peerID -> set of neighbor peerIDs - private val adjacency = ConcurrentHashMap>() - // Latest announcement timestamp per peer (ULong from packet) - private val lastUpdate = ConcurrentHashMap() - - private val _graphState = MutableStateFlow(GraphSnapshot(emptyList(), emptyList())) - val graphState: StateFlow = _graphState.asStateFlow() - - /** - * Update graph from a verified announcement. - * Replaces previous neighbors for origin if this is newer (by timestamp). - */ - fun updateFromAnnouncement(originPeerID: String, originNickname: String?, neighborsOrNull: List?, timestamp: ULong) { - synchronized(this) { - // 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 (may be empty) - val newSet = neighborsOrNull.distinct().take(10).filter { it != originPeerID }.toMutableSet() - adjacency[originPeerID] = newSet - // Ensure undirected edges - newSet.forEach { n -> - adjacency.putIfAbsent(n, mutableSetOf()) - adjacency[n]?.add(originPeerID) - } - - publishSnapshot() - } - } - - fun updateNickname(peerID: String, nickname: String?) { - if (nickname == null) return - nicknames[peerID] = nickname - publishSnapshot() - } - - private fun publishSnapshot() { - val nodes = mutableSetOf() - adjacency.forEach { (a, neighbors) -> - nodes.add(a) - nodes.addAll(neighbors) - } - // Merge in nicknames-only nodes - nodes.addAll(nicknames.keys) - - val nodeList = nodes.map { GraphNode(it, nicknames[it]) }.sortedBy { it.peerID } - val edgeSet = mutableSetOf>() - adjacency.forEach { (a, ns) -> - ns.forEach { b -> - val (x, y) = if (a <= b) a to b else b to a - edgeSet.add(x to y) - } - } - val edges = edgeSet.map { GraphEdge(it.first, it.second) }.sortedWith(compareBy({ it.a }, { it.b })) - _graphState.value = GraphSnapshot(nodeList, edges) - } - - companion object { - @Volatile private var INSTANCE: MeshGraphService? = null - fun getInstance(): MeshGraphService = INSTANCE ?: synchronized(this) { - INSTANCE ?: MeshGraphService().also { INSTANCE = it } - } - } -} diff --git a/app/src/main/java/com/bitchat/android/services/meshgraph/RoutePlanner.kt b/app/src/main/java/com/bitchat/android/services/meshgraph/RoutePlanner.kt deleted file mode 100644 index ae279405..00000000 --- a/app/src/main/java/com/bitchat/android/services/meshgraph/RoutePlanner.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.bitchat.android.services.meshgraph - -import android.util.Log -import java.util.PriorityQueue - -/** - * Computes shortest paths on the current mesh graph snapshot using Dijkstra. - * Assumes unit edge weights. - */ -object RoutePlanner { - private const val TAG = "RoutePlanner" - - /** - * Return full path [src, ..., dst] if reachable, else null. - */ - fun shortestPath(src: String, dst: String): List? { - if (src == dst) return listOf(src) - val snapshot = MeshGraphService.getInstance().graphState.value - val neighbors = mutableMapOf>() - snapshot.edges.forEach { e -> - neighbors.getOrPut(e.a) { mutableSetOf() }.add(e.b) - neighbors.getOrPut(e.b) { mutableSetOf() }.add(e.a) - } - // Ensure nodes known even if isolated - snapshot.nodes.forEach { n -> neighbors.putIfAbsent(n.peerID, mutableSetOf()) } - - if (!neighbors.containsKey(src) || !neighbors.containsKey(dst)) return null - - val dist = mutableMapOf() - val prev = mutableMapOf() - val pq = PriorityQueue>(compareBy { it.second }) - - neighbors.keys.forEach { v -> - dist[v] = if (v == src) 0 else Int.MAX_VALUE - prev[v] = null - } - pq.add(src to 0) - - while (pq.isNotEmpty()) { - val (u, d) = pq.poll() - if (d > (dist[u] ?: Int.MAX_VALUE)) continue - if (u == dst) break - neighbors[u]?.forEach { v -> - val alt = d + 1 - if (alt < (dist[v] ?: Int.MAX_VALUE)) { - dist[v] = alt - prev[v] = u - pq.add(v to alt) - } - } - } - - if ((dist[dst] ?: Int.MAX_VALUE) == Int.MAX_VALUE) return null - - val path = mutableListOf() - var cur: String? = dst - while (cur != null) { - path.add(cur) - cur = prev[cur] - } - path.reverse() - Log.d(TAG, "Computed path $path") - return path - } -} - 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 cdaeee53..03453aaf 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 @@ -24,92 +24,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp 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() { - val colorScheme = MaterialTheme.colorScheme - val graphService = remember { MeshGraphService.getInstance() } - val snapshot by graphService.graphState.collectAsState() - - Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Icon(Icons.Filled.SettingsEthernet, contentDescription = null, tint = Color(0xFF8E8E93)) - Text("mesh topology", fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium) - } - val nodes = snapshot.nodes - val edges = snapshot.edges - val empty = nodes.isEmpty() - if (empty) { - Text("no gossip yet", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.6f)) - } else { - androidx.compose.foundation.Canvas(Modifier.fillMaxWidth().height(220.dp).background(colorScheme.surface.copy(alpha = 0.4f))) { - val w = size.width - val h = size.height - val cx = w / 2f - val cy = h / 2f - val radius = (minOf(w, h) * 0.36f) - val n = nodes.size - if (n == 1) { - // Single node centered - drawCircle(color = Color(0xFF00C851), radius = 12f, center = androidx.compose.ui.geometry.Offset(cx, cy)) - } else { - // Circular layout - val positions = nodes.mapIndexed { i, node -> - val angle = (2 * Math.PI * i.toDouble()) / n - val x = cx + (radius * Math.cos(angle)).toFloat() - val y = cy + (radius * Math.sin(angle)).toFloat() - node.peerID to androidx.compose.ui.geometry.Offset(x, y) - }.toMap() - - // Draw edges - edges.forEach { e -> - val p1 = positions[e.a] - val p2 = positions[e.b] - if (p1 != null && p2 != null) { - drawLine(color = Color(0xFF4A90E2), start = p1, end = p2, strokeWidth = 2f) - } - } - - // Draw nodes - nodes.forEach { node -> - 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 - LazyColumn(modifier = Modifier.fillMaxWidth().heightIn(max = 140.dp)) { - items(nodes) { node -> - val label = "${node.peerID.take(8)} • ${node.nickname ?: "unknown"}" - Text(label, fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.85f)) - } - } - } - } - } -} @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -209,11 +124,6 @@ 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)) { @@ -340,82 +250,39 @@ fun DebugSettingsSheet( } } } - // Y-axis ticks (min/max) in the left margin - Text("0", fontFamily = FontFamily.Monospace, fontSize = 10.sp, color = colorScheme.onSurface.copy(alpha = 0.7f), modifier = Modifier.align(Alignment.BottomStart).padding(start = 4.dp, bottom = 2.dp)) - Text("${maxVal.toInt()}", fontFamily = FontFamily.Monospace, fontSize = 10.sp, color = colorScheme.onSurface.copy(alpha = 0.7f), modifier = Modifier.align(Alignment.TopStart).padding(start = 4.dp, top = 2.dp)) - // Y-axis unit label (vertical) - Text("p/s", fontFamily = FontFamily.Monospace, fontSize = 9.sp, color = colorScheme.onSurface.copy(alpha = 0.7f), modifier = Modifier.align(Alignment.CenterStart).padding(start = 2.dp).rotate(-90f)) - } - } - } -} - -@Composable -fun MeshTopologySection() { - val colorScheme = MaterialTheme.colorScheme - val graphService = remember { MeshGraphService.getInstance() } - val snapshot by graphService.graphState.collectAsState() - - Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Icon(Icons.Filled.SettingsEthernet, contentDescription = null, tint = Color(0xFF8E8E93)) - Text("mesh topology", fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium) - } - val nodes = snapshot.nodes - val edges = snapshot.edges - val empty = nodes.isEmpty() - if (empty) { - Text("no gossip yet", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.6f)) - } else { - androidx.compose.foundation.Canvas(Modifier.fillMaxWidth().height(220.dp).background(colorScheme.surface.copy(alpha = 0.4f))) { - val w = size.width - val h = size.height - val cx = w / 2f - val cy = h / 2f - val radius = (minOf(w, h) * 0.36f) - val n = nodes.size - if (n == 1) { - // Single node centered - drawCircle(color = Color(0xFF00C851), radius = 12f, center = androidx.compose.ui.geometry.Offset(cx, cy)) - } else { - // Circular layout - val positions = nodes.mapIndexed { i, node -> - val angle = (2 * Math.PI * i.toDouble()) / n - val x = cx + (radius * Math.cos(angle)).toFloat() - val y = cy + (radius * Math.sin(angle)).toFloat() - node.peerID to androidx.compose.ui.geometry.Offset(x, y) - }.toMap() - - // Draw edges - edges.forEach { e -> - val p1 = positions[e.a] - val p2 = positions[e.b] - if (p1 != null && p2 != null) { - drawLine(color = Color(0xFF4A90E2), start = p1, end = p2, strokeWidth = 2f) + // Left gutter layout: unit + ticks neatly aligned + Row(Modifier.fillMaxSize()) { + Box(Modifier.width(leftGutter).fillMaxHeight()) { + // Unit label on the far left, centered vertically + Text( + "p/s", + fontFamily = FontFamily.Monospace, + fontSize = 10.sp, + color = colorScheme.onSurface.copy(alpha = 0.7f), + modifier = Modifier.align(Alignment.CenterStart).padding(start = 2.dp).rotate(-90f) + ) + // Tick labels right-aligned in gutter, top and bottom aligned + Text( + "${maxVal.toInt()}", + fontFamily = FontFamily.Monospace, + fontSize = 10.sp, + color = colorScheme.onSurface.copy(alpha = 0.7f), + modifier = Modifier.align(Alignment.TopEnd).padding(end = 4.dp, top = 0.dp) + ) + Text( + "0", + fontFamily = FontFamily.Monospace, + fontSize = 10.sp, + color = colorScheme.onSurface.copy(alpha = 0.7f), + modifier = Modifier.align(Alignment.BottomEnd).padding(end = 4.dp, bottom = 0.dp) + ) + } + Spacer(Modifier.weight(1f)) } } - - // Draw nodes - nodes.forEach { node -> - val pos = positions[node.peerID] ?: androidx.compose.ui.geometry.Offset(cx, cy) - drawCircle(color = Color(0xFF00C851), radius = 10f, center = pos) - } - } - } - // Label list for clarity under the canvas - LazyColumn(modifier = Modifier.fillMaxWidth().heightIn(max = 140.dp)) { - items(nodes) { node -> - val label = "${node.peerID.take(8)} • ${node.nickname ?: "unknown"}" - Text(label, fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.85f)) } } } - } - } -} - - // Connected devices item {