kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
feat: add traceroute (#620)
rodzic
72c278c92c
commit
da5f1d529d
|
@ -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);
|
||||
|
||||
|
|
|
@ -51,4 +51,14 @@ data class MeshLog(@PrimaryKey val uuid: String,
|
|||
return null
|
||||
} ?: nodeInfo?.position
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@ class NodeDB(private val ui: UIViewModel) {
|
|||
private val _nodes = MutableLiveData<Map<String, NodeInfo>>(mapOf(*(if (seedWithTestNodes) testNodes else listOf()).map { it.user!!.id to it }
|
||||
.toTypedArray()))
|
||||
val nodes: LiveData<Map<String, NodeInfo>> get() = _nodes
|
||||
val nodesByNum get() = nodes.value?.values?.associateBy { it.num }
|
||||
|
||||
fun setNodes(nodes: Map<String, NodeInfo>) {
|
||||
_nodes.value = nodes
|
||||
|
|
|
@ -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<MeshLog?>(null)
|
||||
val packetResponse: StateFlow<MeshLog?> = _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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -10,6 +10,10 @@
|
|||
android:id="@+id/request_position"
|
||||
android:title="@string/request_position"
|
||||
app:showAsAction="withText" />
|
||||
<item
|
||||
android:id="@+id/traceroute"
|
||||
android:title="@string/traceroute"
|
||||
app:showAsAction="withText" />
|
||||
</group>
|
||||
<group android:id="@+id/group_admin">
|
||||
<item
|
||||
|
|
|
@ -127,6 +127,7 @@
|
|||
<string name="resend">Resend</string>
|
||||
<string name="shutdown">Shutdown</string>
|
||||
<string name="reboot">Reboot</string>
|
||||
<string name="traceroute">Traceroute</string>
|
||||
<string name="intro_show">Show Introduction</string>
|
||||
<string name="intro_welcome">Welcome to Meshtastic</string>
|
||||
<string name="intro_welcome_text">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.</string>
|
||||
|
|
Ładowanie…
Reference in New Issue