mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2025-12-24 00:07:48 -05:00
Feat request neighbours (#3709)
Signed-off-by: Dane Evans <dane@goneepic.com>
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {}) }
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 = {},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user