mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 12:05:20 +00:00
Gossip mesh topology + source-based routing (#445)
* wip mesh graph * gossip fix * gossip works * source-based routing wip * log * update spec to be explicit about intermediate hops only * drop duplicate hops * add to spec * test * forgot comma * source routing v2 * add compression bomb protection * v2 source routing * fragmented packets inherit route * update spec * Gossip routing tmp with connection limit fixed (#569) * fix: r8 exception for LocationManager (#566) * fragmented packets inherit route * update spec * fix connection limits * fix: deserialization issue with routed packets * log * dynamic fragment size * fragment size * add tests * feat(gossip): implement two-way handshake for source routing edges - Update MeshGraphService to track directed announcements - Require bidirectional announcements for a 'confirmed' edge - Update RoutePlanner to strictly use confirmed edges - Update Mesh Topology debug view to show confirmed vs unconfirmed edges (solid vs dotted) * docs: update SOURCE_ROUTING.md with two-way handshake requirement * evict stale peers from mesh graph service * better logging * fix announce spe * fix: empty route * fix spec * fix: compile error in DebugSettingsSheet and potential NPE in RoutePlanner * revert * try again
This commit is contained in:
@@ -418,7 +418,9 @@ class DebugSettingsManager private constructor() {
|
||||
toNickname: String?,
|
||||
toDeviceAddress: String?,
|
||||
ttl: UByte?,
|
||||
isRelay: Boolean = true
|
||||
isRelay: Boolean = true,
|
||||
packetVersion: UByte = 1u,
|
||||
routeInfo: String? = null
|
||||
) {
|
||||
// Build message only if verbose logging is enabled, but always update stats
|
||||
val senderLabel = when {
|
||||
@@ -441,18 +443,20 @@ class DebugSettingsManager private constructor() {
|
||||
val fromAddr = fromDeviceAddress ?: "?"
|
||||
val toAddr = toDeviceAddress ?: "?"
|
||||
val ttlStr = ttl?.toString() ?: "?"
|
||||
val routeStr = if (routeInfo != null) " $routeInfo" else ""
|
||||
|
||||
if (verboseLoggingEnabled.value) {
|
||||
if (isRelay) {
|
||||
// Relay: show [previousPeer] -> [nextPeer]
|
||||
addDebugMessage(
|
||||
DebugMessage.RelayEvent(
|
||||
"♻️ Relayed $packetType by $senderLabel from $fromName (${fromPeerID ?: "?"}, $fromAddr) to $toName (${toPeerID ?: "?"}, $toAddr) with TTL $ttlStr"
|
||||
"♻️ Relayed v$packetVersion $packetType by $senderLabel from $fromName (${fromPeerID ?: "?"}, $fromAddr) to $toName (${toPeerID ?: "?"}, $toAddr) with TTL $ttlStr$routeStr"
|
||||
)
|
||||
)
|
||||
} else {
|
||||
addDebugMessage(
|
||||
DebugMessage.PacketEvent(
|
||||
"📤 Sent $packetType by $senderLabel to $toName (${toPeerID ?: "?"}, $toAddr) with TTL $ttlStr"
|
||||
"📤 Sent v$packetVersion $packetType by $senderLabel to $toName (${toPeerID ?: "?"}, $toAddr) with TTL $ttlStr$routeStr"
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -462,10 +466,11 @@ class DebugSettingsManager private constructor() {
|
||||
}
|
||||
|
||||
// Explicit incoming/outgoing logging to avoid double counting
|
||||
fun logIncoming(packetType: String, fromPeerID: String?, fromNickname: String?, fromDeviceAddress: String?) {
|
||||
fun logIncoming(packetType: String, fromPeerID: String?, fromNickname: String?, fromDeviceAddress: String?, packetVersion: UByte = 1u, routeInfo: String? = null) {
|
||||
if (verboseLoggingEnabled.value) {
|
||||
val who = fromNickname ?: fromPeerID ?: "unknown"
|
||||
addDebugMessage(DebugMessage.PacketEvent("📥 Incoming $packetType from $who (${fromPeerID ?: "?"}, ${fromDeviceAddress ?: "?"})"))
|
||||
val routeStr = if (routeInfo != null) " $routeInfo" else ""
|
||||
addDebugMessage(DebugMessage.PacketEvent("📥 Incoming v$packetVersion $packetType from $who (${fromPeerID ?: "?"}, ${fromDeviceAddress ?: "?"})$routeStr"))
|
||||
}
|
||||
val now = System.currentTimeMillis()
|
||||
val visible = _debugSheetVisible.value
|
||||
@@ -489,10 +494,11 @@ class DebugSettingsManager private constructor() {
|
||||
if (visible) updateRelayStatsFromTimestamps()
|
||||
}
|
||||
|
||||
fun logOutgoing(packetType: String, toPeerID: String?, toNickname: String?, toDeviceAddress: String?, previousHopPeerID: String? = null) {
|
||||
fun logOutgoing(packetType: String, toPeerID: String?, toNickname: String?, toDeviceAddress: String?, previousHopPeerID: String? = null, packetVersion: UByte = 1u, routeInfo: String? = null) {
|
||||
if (verboseLoggingEnabled.value) {
|
||||
val who = toNickname ?: toPeerID ?: "unknown"
|
||||
addDebugMessage(DebugMessage.PacketEvent("📤 Outgoing $packetType to $who (${toPeerID ?: "?"}, ${toDeviceAddress ?: "?"})"))
|
||||
val routeStr = if (routeInfo != null) " $routeInfo" else ""
|
||||
addDebugMessage(DebugMessage.PacketEvent("📤 Outgoing v$packetVersion $packetType to $who (${toPeerID ?: "?"}, ${toDeviceAddress ?: "?"})$routeStr"))
|
||||
}
|
||||
val now = System.currentTimeMillis()
|
||||
val visible = _debugSheetVisible.value
|
||||
|
||||
@@ -25,12 +25,120 @@ 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
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.bitchat.android.R
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.bitchat.android.core.ui.component.sheet.BitchatBottomSheet
|
||||
|
||||
@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) {
|
||||
if (e.isConfirmed) {
|
||||
drawLine(color = Color(0xFF4A90E2), start = p1, end = p2, strokeWidth = 2f)
|
||||
} else {
|
||||
// Unconfirmed: draw "solid" from declarer, "dashed" from other
|
||||
val start = if (e.confirmedBy == e.a) p1 else p2
|
||||
val end = if (e.confirmedBy == e.a) p2 else p1
|
||||
|
||||
val midX = (start.x + end.x) / 2
|
||||
val midY = (start.y + end.y) / 2
|
||||
val mid = androidx.compose.ui.geometry.Offset(midX, midY)
|
||||
|
||||
// Solid half
|
||||
drawLine(color = Color(0xFF4A90E2), start = start, end = mid, strokeWidth = 2f)
|
||||
|
||||
// Dotted half
|
||||
drawLine(
|
||||
color = Color(0xFF4A90E2),
|
||||
start = mid,
|
||||
end = end,
|
||||
strokeWidth = 2f,
|
||||
pathEffect = androidx.compose.ui.graphics.PathEffect.dashPathEffect(floatArrayOf(5f, 5f), 0f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.size) { i ->
|
||||
val node = nodes[i]
|
||||
val label = "${node.peerID.take(8)} • ${node.nickname ?: "unknown"}"
|
||||
Text(label, fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.85f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class GraphMode { OVERALL, PER_DEVICE, PER_PEER }
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@@ -139,6 +247,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)) {
|
||||
@@ -348,7 +461,7 @@ fun DebugSettingsSheet(
|
||||
kotlinx.coroutines.delay(1000)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Helper functions moved to top-level composable below to avoid scope issues
|
||||
|
||||
// Render two blocks: Incoming and Outgoing
|
||||
@@ -488,9 +601,6 @@ fun DebugSettingsSheet(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Connected devices
|
||||
item {
|
||||
Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) {
|
||||
|
||||
Reference in New Issue
Block a user