Feat request neighbours (#3709)

Signed-off-by: Dane Evans <dane@goneepic.com>
This commit is contained in:
Dane Evans
2025-12-22 07:45:06 +11:00
committed by GitHub
parent 3e3dfe08e6
commit d33229c50f
14 changed files with 375 additions and 10 deletions

View File

@@ -266,6 +266,13 @@ constructor(
serviceRepository.clearTracerouteResponse()
}
val neighborInfoResponse: LiveData<String?>
get() = serviceRepository.neighborInfoResponse.asLiveData()
fun clearNeighborInfoResponse() {
serviceRepository.clearNeighborInfoResponse()
}
val appIntroCompleted: StateFlow<Boolean> = uiPreferencesDataSource.appIntroCompleted
fun onAppIntroCompleted() {

View File

@@ -179,6 +179,9 @@ class MeshService : Service() {
@Inject lateinit var analytics: PlatformAnalytics
private val tracerouteStartTimes = ConcurrentHashMap<Int, Long>()
private val neighborInfoStartTimes = ConcurrentHashMap<Int, Long>()
@Volatile private var lastNeighborInfo: MeshProtos.NeighborInfo? = null
private val logUuidByPacketId = ConcurrentHashMap<Int, String>()
private val logInsertJobByPacketId = ConcurrentHashMap<Int, Job>()
@@ -241,6 +244,8 @@ class MeshService : Service() {
private const val DEFAULT_HISTORY_RETURN_MAX_MESSAGES = 100
private const val MAX_EARLY_PACKET_BUFFER = 128
private const val NEIGHBOR_RQ_COOLDOWN = 3 * 60 * 1000L // ms
@VisibleForTesting
internal fun buildStoreForwardHistoryRequest(
lastRequest: Int,
@@ -305,6 +310,8 @@ class MeshService : Service() {
private val batteryPercentCooldownSeconds = 1500
private val batteryPercentCooldowns: HashMap<Int, Long> = HashMap()
private val oneHour = 3600
private fun getSenderName(packet: DataPacket?): String {
val name = nodeDBbyID[packet?.from]?.user?.longName
return name ?: getString(Res.string.unknown_username)
@@ -995,6 +1002,138 @@ class MeshService : Service() {
}
}
Portnums.PortNum.NEIGHBORINFO_APP_VALUE -> {
val requestId = packet.decoded.requestId
Timber.d("Processing NEIGHBORINFO_APP packet with requestId: $requestId")
val start = neighborInfoStartTimes.remove(requestId)
Timber.d("Found start time for requestId $requestId: $start")
val info =
runCatching { MeshProtos.NeighborInfo.parseFrom(data.payload.toByteArray()) }.getOrNull()
// Store the last neighbor info from our connected radio
if (info != null && packet.from == myInfo.myNodeNum) {
lastNeighborInfo = info
Timber.d("Stored last neighbor info from connected radio")
}
// Only show response if packet is addressed to us and we sent a request in the last 3 minutes
val isAddressedToUs = packet.to == myInfo.myNodeNum
val isRecentRequest =
start != null && (System.currentTimeMillis() - start) < NEIGHBOR_RQ_COOLDOWN
if (isAddressedToUs && isRecentRequest) {
val formatted =
if (info != null) {
val fmtNode: (Int) -> String = { nodeNum ->
val user = nodeRepository.nodeDBbyNum.value[nodeNum]?.user
val shortName = user?.shortName?.takeIf { it.isNotEmpty() } ?: ""
val nodeId = "!%08x".format(nodeNum)
if (shortName.isNotEmpty()) "$nodeId ($shortName)" else nodeId
}
buildString {
appendLine("NeighborInfo:")
appendLine("node_id: ${fmtNode(info.nodeId)}")
appendLine("last_sent_by_id: ${fmtNode(info.lastSentById)}")
appendLine("node_broadcast_interval_secs: ${info.nodeBroadcastIntervalSecs}")
if (info.neighborsCount > 0) {
appendLine("neighbors:")
info.neighborsList.forEach { n ->
appendLine(" - node_id: ${fmtNode(n.nodeId)} snr: ${n.snr}")
}
}
}
} else {
// Fallback to raw string if parsing fails
String(data.payload.toByteArray())
}
val response =
if (start != null) {
val elapsedMs = System.currentTimeMillis() - start
val seconds = elapsedMs / 1000.0
Timber.i("Neighbor info $requestId complete in $seconds s")
"$formatted\n\nDuration: ${"%.1f".format(seconds)} s"
} else {
Timber.w("No start time found for neighbor info requestId: $requestId")
formatted
}
serviceRepository.setNeighborInfoResponse(response)
} else {
Timber.d(
"Neighbor info response filtered: ToUs=%s, isRecentRequest=%s",
isAddressedToUs,
isRecentRequest,
)
}
}
Portnums.PortNum.NEIGHBORINFO_APP_VALUE -> {
val requestId = packet.decoded.requestId
Timber.d("Processing NEIGHBORINFO_APP packet with requestId: $requestId")
val start = neighborInfoStartTimes.remove(requestId)
Timber.d("Found start time for requestId $requestId: $start")
val info =
runCatching { MeshProtos.NeighborInfo.parseFrom(data.payload.toByteArray()) }.getOrNull()
// Store the last neighbor info from our connected radio
if (info != null && packet.from == myInfo.myNodeNum) {
lastNeighborInfo = info
Timber.d("Stored last neighbor info from connected radio")
}
// Only show response if packet is addressed to us and we sent a request in the last 3 minutes
val isAddressedToUs = packet.to == myInfo.myNodeNum
val isRecentRequest =
start != null && (System.currentTimeMillis() - start) < NEIGHBOR_RQ_COOLDOWN
if (isAddressedToUs && isRecentRequest) {
val formatted =
if (info != null) {
val fmtNode: (Int) -> String = { nodeNum ->
val user = nodeRepository.nodeDBbyNum.value[nodeNum]?.user
val shortName = user?.shortName?.takeIf { it.isNotEmpty() } ?: ""
val nodeId = "!%08x".format(nodeNum)
if (shortName.isNotEmpty()) "$nodeId ($shortName)" else nodeId
}
buildString {
appendLine("NeighborInfo:")
appendLine("node_id: ${fmtNode(info.nodeId)}")
appendLine("last_sent_by_id: ${fmtNode(info.lastSentById)}")
appendLine("node_broadcast_interval_secs: ${info.nodeBroadcastIntervalSecs}")
if (info.neighborsCount > 0) {
appendLine("neighbors:")
info.neighborsList.forEach { n ->
appendLine(" - node_id: ${fmtNode(n.nodeId)} snr: ${n.snr}")
}
}
}
} else {
// Fallback to raw string if parsing fails
String(data.payload.toByteArray())
}
val response =
if (start != null) {
val elapsedMs = System.currentTimeMillis() - start
val seconds = elapsedMs / 1000.0
Timber.i("Neighbor info $requestId complete in $seconds s")
"$formatted\n\nDuration: ${"%.1f".format(seconds)} s"
} else {
Timber.w("No start time found for neighbor info requestId: $requestId")
formatted
}
serviceRepository.setNeighborInfoResponse(response)
} else {
Timber.d(
"Neighbor info response filtered: isToUs=%s, isRecent=%s",
isAddressedToUs,
isRecentRequest,
)
}
}
else -> Timber.d("No custom processing needed for ${data.portnumValue} from $fromId")
}
@@ -2649,6 +2788,45 @@ class MeshService : Service() {
}
}
override fun requestNeighborInfo(requestId: Int, destNum: Int) = toRemoteExceptions {
if (destNum != myNodeNum) {
neighborInfoStartTimes[requestId] = System.currentTimeMillis()
// Always send the neighbor info from our connected radio (myNodeNum), not request from destNum
val neighborInfoToSend =
lastNeighborInfo
?: run {
// If we don't have it, send dummy/interceptable data
Timber.d("No stored neighbor info from connected radio, sending dummy data")
MeshProtos.NeighborInfo.newBuilder()
.setNodeId(myNodeNum)
.setLastSentById(myNodeNum)
.setNodeBroadcastIntervalSecs(oneHour)
.addNeighbors(
MeshProtos.Neighbor.newBuilder()
.setNodeId(0) // Dummy node ID that can be intercepted
.setSnr(0f)
.setLastRxTime(currentSecond())
.setNodeBroadcastIntervalSecs(oneHour)
.build(),
)
.build()
}
// Send the neighbor info from our connected radio to the destination
packetHandler.sendToRadio(
newMeshPacketTo(destNum).buildMeshPacket(
wantAck = true,
id = requestId,
channel = nodeDBbyNodeNum[destNum]?.channel ?: 0,
) {
portnumValue = Portnums.PortNum.NEIGHBORINFO_APP_VALUE
payload = neighborInfoToSend.toByteString()
wantResponse = true
},
)
}
}
override fun requestPosition(destNum: Int, position: Position) = toRemoteExceptions {
if (destNum != myNodeNum) {
val provideLocation = meshPrefs.shouldProvideNodeLocation(myNodeNum)

View File

@@ -33,8 +33,10 @@ import androidx.compose.animation.scaleOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.recalculateWindowInsets
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.width
@@ -47,6 +49,7 @@ import androidx.compose.material3.BadgedBox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Text
@@ -74,6 +77,7 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -132,6 +136,7 @@ import org.meshtastic.core.strings.firmware_old
import org.meshtastic.core.strings.firmware_too_old
import org.meshtastic.core.strings.map
import org.meshtastic.core.strings.must_update
import org.meshtastic.core.strings.neighbor_info
import org.meshtastic.core.strings.nodes
import org.meshtastic.core.strings.okay
import org.meshtastic.core.strings.should_update
@@ -293,6 +298,98 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
onDismiss = { tracerouteMapError = null },
)
}
val neighborInfoResponse by uIViewModel.neighborInfoResponse.observeAsState()
neighborInfoResponse?.let { response ->
SimpleAlertDialog(
title = Res.string.neighbor_info,
text = {
Column(modifier = Modifier.fillMaxWidth()) {
fun tryParseNeighborInfo(input: String): MeshProtos.NeighborInfo? {
// First, try parsing directly from raw bytes of the string
var neighborInfo: MeshProtos.NeighborInfo? =
runCatching { MeshProtos.NeighborInfo.parseFrom(input.toByteArray()) }.getOrNull()
if (neighborInfo == null) {
// Next, try to decode a hex dump embedded as text (e.g., "AA BB CC ...")
val hexPairs = Regex("""\b[0-9A-Fa-f]{2}\b""").findAll(input).map { it.value }.toList()
@Suppress("detekt:MagicNumber") // byte offsets
if (hexPairs.size >= 4) {
val bytes = hexPairs.map { it.toInt(16).toByte() }.toByteArray()
neighborInfo = runCatching { MeshProtos.NeighborInfo.parseFrom(bytes) }.getOrNull()
}
}
return neighborInfo
}
val parsed = tryParseNeighborInfo(response)
if (parsed != null) {
fun fmtNode(nodeNum: Int): String = "!%08x".format(nodeNum)
Text(text = "NeighborInfo:", style = MaterialTheme.typography.bodyMedium)
Text(
text = "node_id: ${fmtNode(parsed.nodeId)}",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 8.dp),
)
Text(
text = "last_sent_by_id: ${fmtNode(parsed.lastSentById)}",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 2.dp),
)
Text(
text = "node_broadcast_interval_secs: ${parsed.nodeBroadcastIntervalSecs}",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 2.dp),
)
if (parsed.neighborsCount > 0) {
Text(
text = "neighbors:",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 4.dp),
)
parsed.neighborsList.forEach { n ->
Text(
text = " - node_id: ${fmtNode(n.nodeId)} snr: ${n.snr}",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 8.dp),
)
}
}
} else {
val rawBytes = response.toByteArray()
@Suppress("detekt:MagicNumber") // byte offsets
val isBinary = response.any { it.code < 32 && it != '\n' && it != '\r' && it != '\t' }
if (isBinary) {
val hexString = rawBytes.joinToString(" ") { "%02X".format(it) }
Text(
text = "Binary data (hex view):",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 4.dp),
)
Text(
text = hexString,
style =
MaterialTheme.typography.bodyMedium.copy(
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
),
modifier = Modifier.padding(bottom = 8.dp).fillMaxWidth(),
)
} else {
Text(
text = response,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 8.dp).fillMaxWidth(),
)
}
}
}
},
dismissText = stringResource(Res.string.okay),
onDismiss = { uIViewModel.clearNeighborInfoResponse() },
)
}
val navSuiteType = NavigationSuiteScaffoldDefaults.navigationSuiteType(currentWindowAdaptiveInfo())
val currentDestination = navController.currentBackStackEntryAsState().value?.destination
val topLevelDestination = TopLevelDestination.fromNavDestination(currentDestination)

View File

@@ -126,6 +126,9 @@ interface IMeshService {
/// Send traceroute packet with wantResponse to nodeNum
void requestTraceroute(in int requestId, in int destNum);
/// Send neighbor info packet with wantResponse to nodeNum
void requestNeighborInfo(in int requestId, in int destNum);
/// Send Shutdown admin packet to nodeNum
void requestShutdown(in int requestId, in int destNum);

View File

@@ -118,6 +118,18 @@ class ServiceRepository @Inject constructor() {
setTracerouteResponse(null)
}
private val _neighborInfoResponse = MutableStateFlow<String?>(null)
val neighborInfoResponse: StateFlow<String?>
get() = _neighborInfoResponse
fun setNeighborInfoResponse(value: String?) {
_neighborInfoResponse.value = value
}
fun clearNeighborInfoResponse() {
setNeighborInfoResponse(null)
}
private val _serviceAction = Channel<ServiceAction>()
val serviceAction = _serviceAction.receiveAsFlow()

View File

@@ -742,6 +742,7 @@
<string name="import_known_shared_contact_text">Warning: This contact is known, importing will overwrite the previous contact information.</string>
<string name="public_key_changed">Public Key Changed</string>
<string name="import_label">Import</string>
<string name="request_neighbor_info">Request NeighborInfo (2.7.15+)</string>
<string name="request_metadata">Request Metadata</string>
<string name="actions">Actions</string>
<string name="firmware">Firmware</string>

View File

@@ -125,10 +125,10 @@ public class RadiusMarkerClusterer extends MarkerClusterer {
Iterator<MarkerWithLabel> it = mClonedMarkers.iterator();
while (it.hasNext()) {
MarkerWithLabel neighbour = it.next();
double distance = clusterPosition.distanceToAsDouble(neighbour.getPosition());
MarkerWithLabel neighbor = it.next();
double distance = clusterPosition.distanceToAsDouble(neighbor.getPosition());
if (distance <= mRadiusInMeters) {
cluster.add(neighbour);
cluster.add(neighbor);
it.remove();
}
}

View File

@@ -22,6 +22,7 @@ import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Route
import androidx.compose.material.icons.twotone.Mediation
import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.Composable
@@ -30,16 +31,19 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.request_neighbor_info
import org.meshtastic.core.strings.traceroute
import org.meshtastic.core.ui.component.BasicListItem
import org.meshtastic.core.ui.theme.AppTheme
private const val COOL_DOWN_TIME_MS = 30000L
private const val REQUEST_NEIGHBORS_COOL_DOWN_TIME_MS = 180000L // 3 minutes
@Composable
fun TracerouteButton(
@@ -61,20 +65,42 @@ fun TracerouteButton(
}
}
TracerouteButton(text = text, progress = progress.value, onClick = onClick)
CooldownButton(text = text, leadingIcon = Icons.Default.Route, progress = progress.value, onClick = onClick)
}
@Composable
fun RequestNeighborsButton(
text: String = stringResource(Res.string.request_neighbor_info),
lastRequestNeighborsTime: Long?,
onClick: () -> Unit,
) {
val progress = remember { Animatable(0f) }
LaunchedEffect(lastRequestNeighborsTime) {
val timeSinceLast = System.currentTimeMillis() - (lastRequestNeighborsTime ?: 0)
if (timeSinceLast < REQUEST_NEIGHBORS_COOL_DOWN_TIME_MS) {
val remainingTime = REQUEST_NEIGHBORS_COOL_DOWN_TIME_MS - timeSinceLast
progress.snapTo(remainingTime / REQUEST_NEIGHBORS_COOL_DOWN_TIME_MS.toFloat())
progress.animateTo(
targetValue = 0f,
animationSpec = tween(durationMillis = remainingTime.toInt(), easing = { it }),
)
}
}
CooldownButton(text = text, leadingIcon = Icons.TwoTone.Mediation, progress = progress.value, onClick = onClick)
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun TracerouteButton(text: String, progress: Float, onClick: () -> Unit) {
private fun CooldownButton(text: String, leadingIcon: ImageVector, progress: Float, onClick: () -> Unit) {
val isCoolingDown = progress > 0f
val stroke = Stroke(width = with(LocalDensity.current) { 2.dp.toPx() }, cap = StrokeCap.Round)
BasicListItem(
text = text,
enabled = !isCoolingDown,
leadingIcon = Icons.Default.Route,
leadingIcon = leadingIcon,
trailingContent = {
if (isCoolingDown) {
CircularWavyProgressIndicator(
@@ -97,5 +123,5 @@ private fun TracerouteButton(text: String, progress: Float, onClick: () -> Unit)
@Preview(showBackground = true)
@Composable
private fun TracerouteButtonPreview() {
AppTheme { TracerouteButton(text = "Traceroute", progress = .6f, onClick = {}) }
AppTheme { CooldownButton(text = "Traceroute", leadingIcon = Icons.Default.Route, progress = .6f, onClick = {}) }
}

View File

@@ -50,6 +50,7 @@ import org.meshtastic.feature.node.model.NodeDetailAction
fun DeviceActions(
node: Node,
lastTracerouteTime: Long?,
lastRequestNeighborsTime: Long?,
onAction: (NodeDetailAction) -> Unit,
modifier: Modifier = Modifier,
isLocal: Boolean = false,
@@ -81,7 +82,12 @@ fun DeviceActions(
)
if (!isLocal) {
InsetDivider()
RemoteDeviceActions(node = node, lastTracerouteTime = lastTracerouteTime, onAction = onAction)
RemoteDeviceActions(
node = node,
lastTracerouteTime = lastTracerouteTime,
lastRequestNeighborsTime = lastRequestNeighborsTime,
onAction = onAction,
)
}
InsetDivider()

View File

@@ -96,6 +96,8 @@ sealed class NodeMenuAction {
data class RequestUserInfo(val node: Node) : NodeMenuAction()
data class RequestNeighborInfo(val node: Node) : NodeMenuAction()
data class RequestPosition(val node: Node) : NodeMenuAction()
data class TraceRoute(val node: Node) : NodeMenuAction()

View File

@@ -32,7 +32,12 @@ import org.meshtastic.feature.node.model.NodeDetailAction
import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
@Composable
internal fun RemoteDeviceActions(node: Node, lastTracerouteTime: Long?, onAction: (NodeDetailAction) -> Unit) {
internal fun RemoteDeviceActions(
node: Node,
lastTracerouteTime: Long?,
lastRequestNeighborsTime: Long?,
onAction: (NodeDetailAction) -> Unit,
) {
if (!node.isEffectivelyUnmessageable) {
ListItem(
text = stringResource(Res.string.direct_message),
@@ -57,4 +62,8 @@ internal fun RemoteDeviceActions(node: Node, lastTracerouteTime: Long?, onAction
lastTracerouteTime = lastTracerouteTime,
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.TraceRoute(node))) },
)
RequestNeighborsButton(
lastRequestNeighborsTime = lastRequestNeighborsTime,
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.RequestNeighborInfo(node))) },
)
}

View File

@@ -74,6 +74,7 @@ fun NodeDetailContent(
ourNode: Node?,
metricsState: MetricsState,
lastTracerouteTime: Long?,
lastRequestNeighborsTime: Long?,
availableLogs: Set<LogsType>,
onAction: (NodeDetailAction) -> Unit,
onSaveNotes: (nodeNum: Int, notes: String) -> Unit,
@@ -87,6 +88,7 @@ fun NodeDetailContent(
NodeDetailList(
node = node,
lastTracerouteTime = lastTracerouteTime,
lastRequestNeighborsTime = lastRequestNeighborsTime,
ourNode = ourNode,
metricsState = metricsState,
onAction = { action ->
@@ -108,6 +110,7 @@ fun NodeDetailContent(
fun NodeDetailList(
node: Node,
lastTracerouteTime: Long?,
lastRequestNeighborsTime: Long?,
ourNode: Node?,
metricsState: MetricsState,
onAction: (NodeDetailAction) -> Unit,
@@ -165,6 +168,7 @@ fun NodeDetailList(
DeviceActions(
isLocal = metricsState.isLocal,
lastTracerouteTime = lastTracerouteTime,
lastRequestNeighborsTime = lastRequestNeighborsTime,
node = node,
onAction = onAction,
)
@@ -263,6 +267,7 @@ private fun NodeDetailsPreview(@PreviewParameter(NodePreviewParameterProvider::c
node = node,
ourNode = node,
lastTracerouteTime = null,
lastRequestNeighborsTime = null,
metricsState = MetricsState.Companion.Empty,
availableLogs = emptySet(),
onAction = {},

View File

@@ -56,6 +56,7 @@ fun NodeDetailScreen(
val state by viewModel.state.collectAsStateWithLifecycle()
val environmentState by viewModel.environmentState.collectAsStateWithLifecycle()
val lastTracerouteTime by nodeDetailViewModel.lastTraceRouteTime.collectAsStateWithLifecycle()
val lastRequestNeighborsTime by nodeDetailViewModel.lastRequestNeighborsTime.collectAsStateWithLifecycle()
val ourNode by nodeDetailViewModel.ourNodeInfo.collectAsStateWithLifecycle()
val availableLogs by
@@ -100,6 +101,7 @@ fun NodeDetailScreen(
ourNode = ourNode,
metricsState = state,
lastTracerouteTime = lastTracerouteTime,
lastRequestNeighborsTime = lastRequestNeighborsTime,
availableLogs = availableLogs,
onAction = { action ->
handleNodeAction(

View File

@@ -48,12 +48,19 @@ constructor(
private val _lastTraceRouteTime = MutableStateFlow<Long?>(null)
val lastTraceRouteTime: StateFlow<Long?> = _lastTraceRouteTime.asStateFlow()
private val _lastRequestNeighborsTime = MutableStateFlow<Long?>(null)
val lastRequestNeighborsTime: StateFlow<Long?> = _lastRequestNeighborsTime.asStateFlow()
fun handleNodeMenuAction(action: NodeMenuAction) {
when (action) {
is NodeMenuAction.Remove -> removeNode(action.node.num)
is NodeMenuAction.Ignore -> ignoreNode(action.node)
is NodeMenuAction.Favorite -> favoriteNode(action.node)
is NodeMenuAction.RequestUserInfo -> requestUserInfo(action.node.num)
is NodeMenuAction.RequestNeighborInfo -> {
requestNeighborInfo(action.node.num)
_lastRequestNeighborsTime.value = System.currentTimeMillis()
}
is NodeMenuAction.RequestPosition -> requestPosition(action.node.num)
is NodeMenuAction.TraceRoute -> {
requestTraceroute(action.node.num)
@@ -110,6 +117,16 @@ constructor(
}
}
private fun requestNeighborInfo(destNum: Int) {
Timber.i("Requesting NeighborInfo for '$destNum'")
try {
val packetId = serviceRepository.meshService?.packetId ?: return
serviceRepository.meshService?.requestNeighborInfo(packetId, destNum)
} catch (ex: RemoteException) {
Timber.e("Request NeighborInfo error: ${ex.message}")
}
}
private fun requestPosition(destNum: Int, position: Position = Position(0.0, 0.0, 0)) {
Timber.i("Requesting position for '$destNum'")
try {