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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 375 additions and 10 deletions

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 {