From da5f1d529dbbe2f4afc10962cd9c91f12ef70dd5 Mon Sep 17 00:00:00 2001 From: Andre K Date: Sun, 16 Apr 2023 06:16:41 -0300 Subject: [PATCH] feat: add traceroute (#620) --- .../com/geeksville/mesh/IMeshService.aidl | 3 ++ .../mesh/database/entity/MeshLog.kt | 12 +++++- .../java/com/geeksville/mesh/model/NodeDB.kt | 1 + .../java/com/geeksville/mesh/model/UIState.kt | 40 +++++++++++++++++++ .../geeksville/mesh/service/MeshService.kt | 10 ++++- .../com/geeksville/mesh/ui/UsersFragment.kt | 26 ++++++++++++ app/src/main/res/menu/menu_nodes.xml | 4 ++ app/src/main/res/values/strings.xml | 1 + 8 files changed, 95 insertions(+), 2 deletions(-) diff --git a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl index cad2ba775..36114daf3 100644 --- a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl +++ b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl @@ -104,6 +104,9 @@ interface IMeshService { /// Send position packet with wantResponse to nodeNum void requestPosition(in int idNum, in Position position); + /// Send traceroute packet with wantResponse to nodeNum + void requestTraceroute(in int requestId, in int destNum); + /// Send Shutdown admin packet to nodeNum void requestShutdown(in int idNum); diff --git a/app/src/main/java/com/geeksville/mesh/database/entity/MeshLog.kt b/app/src/main/java/com/geeksville/mesh/database/entity/MeshLog.kt index 7b03ff5b1..b0bc0570c 100644 --- a/app/src/main/java/com/geeksville/mesh/database/entity/MeshLog.kt +++ b/app/src/main/java/com/geeksville/mesh/database/entity/MeshLog.kt @@ -51,4 +51,14 @@ data class MeshLog(@PrimaryKey val uuid: String, return null } ?: nodeInfo?.position } -} \ No newline at end of file + + val routeDiscovery: MeshProtos.RouteDiscovery? + get() { + return meshPacket?.run { + if (hasDecoded() && decoded.portnumValue == Portnums.PortNum.TRACEROUTE_APP_VALUE) { + return MeshProtos.RouteDiscovery.parseFrom(decoded.payload) + } + return null + } + } +} diff --git a/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt b/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt index 9b2ab9861..2bf0cbcfb 100644 --- a/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt +++ b/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt @@ -56,6 +56,7 @@ class NodeDB(private val ui: UIViewModel) { private val _nodes = MutableLiveData>(mapOf(*(if (seedWithTestNodes) testNodes else listOf()).map { it.user!!.id to it } .toTypedArray())) val nodes: LiveData> get() = _nodes + val nodesByNum get() = nodes.value?.values?.associateBy { it.num } fun setNodes(nodes: Map) { _nodes.value = nodes diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 692d2c26f..838ae3be0 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -34,6 +34,7 @@ import com.geeksville.mesh.util.positionToMeter import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -44,6 +45,7 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull import org.osmdroid.bonuspack.kml.KmlDocument import org.osmdroid.views.MapView import org.osmdroid.views.overlay.FolderOverlay @@ -172,6 +174,44 @@ class UIViewModel @Inject constructor( .filterValues { it.data.waypoint!!.expire > System.currentTimeMillis() / 1000 } }.asLiveData() + private val _packetResponse = MutableStateFlow(null) + val packetResponse: StateFlow = _packetResponse + + /** + * Called immediately after activity observes packetResponse + */ + fun clearPacketResponse() { + _packetResponse.tryEmit(null) + } + + /** + * Returns the packet response to a given [packetId] or null after [timeout] milliseconds + */ + private suspend fun getResponseBy(packetId: Int, timeout: Long) = withContext(Dispatchers.IO) { + withTimeoutOrNull(timeout) { + var packet: MeshLog? = null + while (packet == null) { + packet = _meshLog.value.lastOrNull { it.meshPacket?.decoded?.requestId == packetId } + if (packet == null) delay(1000) + } + packet + } + } + + fun requestTraceroute(destNum: Int) = viewModelScope.launch { + meshService?.let { service -> + try { + val packetId = service.packetId + val waitFactor = (service.nodes.count { it.isOnline } - 1) + .coerceAtMost(config.lora.hopLimit) + service.requestTraceroute(packetId, destNum) + _packetResponse.emit(getResponseBy(packetId, 20000L * waitFactor)) + } catch (ex: RemoteException) { + errormsg("Request traceroute error: ${ex.message}") + } + } + } + fun generatePacketId(): Int? { return try { meshService?.packetId diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index c61862617..8ea1dd3eb 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -1734,13 +1734,21 @@ class MeshService : Service(), Logging { override fun requestPosition(idNum: Int, position: Position) = toRemoteExceptions { - val (lat, lon, alt) = with(position) { Triple(latitude, longitude, altitude) } + val (lat, lon, alt) = position // request position if (idNum != 0) sendPosition(time = 1, destNum = idNum, wantResponse = true) // set local node's fixed position else sendPosition(time = 0, destNum = null, lat = lat, lon = lon, alt = alt) } + override fun requestTraceroute(requestId: Int, destNum: Int) = toRemoteExceptions { + sendToRadio(newMeshPacketTo(destNum).buildMeshPacket(id = requestId) { + portnumValue = Portnums.PortNum.TRACEROUTE_APP_VALUE + payload = routeDiscovery {}.toByteString() + wantResponse = true + }) + } + override fun requestShutdown(idNum: Int) = toRemoteExceptions { sendToRadio(newMeshPacketTo(idNum).buildAdminPacket { shutdownSeconds = 5 diff --git a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt index 832ff5a29..b769987c1 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt @@ -13,6 +13,7 @@ import androidx.core.os.bundleOf import androidx.core.text.HtmlCompat import androidx.fragment.app.activityViewModels import androidx.fragment.app.setFragmentResult +import androidx.lifecycle.asLiveData import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.geeksville.mesh.NodeInfo @@ -86,6 +87,12 @@ class UsersFragment : ScreenFragment("Users"), Logging { model.requestPosition(node.num) } } + R.id.traceroute -> { + if (position > 0 && user != null) { + debug("requesting traceroute for ${user.longName}") + model.requestTraceroute(node.num) + } + } R.id.reboot -> { MaterialAlertDialogBuilder(requireContext()) .setTitle("${getString(R.string.reboot)}\n${user?.longName}?") @@ -323,6 +330,25 @@ class UsersFragment : ScreenFragment("Users"), Logging { model.nodeDB.nodes.observe(viewLifecycleOwner) { nodesAdapter.onNodesChanged(it.values.toTypedArray()) } + + model.packetResponse.asLiveData().observe(viewLifecycleOwner) { meshLog -> + meshLog?.meshPacket?.let { meshPacket -> + val routeList = meshLog.routeDiscovery?.routeList + fun nodeName(num: Int) = model.nodeDB.nodesByNum?.get(num)?.user?.longName + + var routeStr = "${nodeName(meshPacket.from)} --> " + routeList?.forEach { num -> routeStr += "${nodeName(num)} --> " } + routeStr += "${nodeName(meshPacket.to)}" + + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.traceroute) + .setMessage(routeStr) + .setPositiveButton(R.string.okay) { _, _ -> } + .show() + + model.clearPacketResponse() + } + } } override fun onDestroyView() { diff --git a/app/src/main/res/menu/menu_nodes.xml b/app/src/main/res/menu/menu_nodes.xml index 3dfbf14f1..ab4d71003 100644 --- a/app/src/main/res/menu/menu_nodes.xml +++ b/app/src/main/res/menu/menu_nodes.xml @@ -10,6 +10,10 @@ android:id="@+id/request_position" android:title="@string/request_position" app:showAsAction="withText" /> + Resend Shutdown Reboot + Traceroute Show Introduction Welcome to Meshtastic Meshtastic is an open-source, off-grid, encrypted communication platform. The Meshtastic radios form a mesh network and communicate using the LoRa protocol to send text messages.