mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-05-12 16:55:02 -04:00
feat: Queue special app PortNums when disconnected (#4495)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
@@ -76,7 +76,15 @@ constructor(
|
||||
@Volatile var lastNeighborInfo: NeighborInfo? = null
|
||||
|
||||
private val rememberDataType =
|
||||
setOf(PortNum.TEXT_MESSAGE_APP.value, PortNum.ALERT_APP.value, PortNum.WAYPOINT_APP.value)
|
||||
setOf(
|
||||
PortNum.TEXT_MESSAGE_APP.value,
|
||||
PortNum.ALERT_APP.value,
|
||||
PortNum.WAYPOINT_APP.value,
|
||||
PortNum.ATAK_PLUGIN.value,
|
||||
PortNum.ATAK_FORWARDER.value,
|
||||
PortNum.DETECTION_SENSOR_APP.value,
|
||||
PortNum.PRIVATE_APP.value,
|
||||
)
|
||||
|
||||
fun start(scope: CoroutineScope) {
|
||||
this.scope = scope
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import okio.ByteString
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.proto.PortNum
|
||||
|
||||
class MeshCommandSenderQueueTest {
|
||||
|
||||
private val packetHandler = mockk<PacketHandler>(relaxed = true)
|
||||
private val connectionStateHandler = mockk<ConnectionStateHandler>(relaxed = true)
|
||||
private val connectionStateFlow = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
|
||||
|
||||
private lateinit var commandSender: MeshCommandSender
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
every { connectionStateHandler.connectionState } returns connectionStateFlow.asStateFlow()
|
||||
commandSender = MeshCommandSender(packetHandler, null, connectionStateHandler, null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sendData queues TEXT_MESSAGE_APP when disconnected`() {
|
||||
val packet = DataPacket(dataType = PortNum.TEXT_MESSAGE_APP.value, bytes = ByteString.EMPTY)
|
||||
commandSender.sendData(packet)
|
||||
|
||||
verify(exactly = 0) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
|
||||
connectionStateFlow.value = ConnectionState.Connected
|
||||
commandSender.processQueuedPackets()
|
||||
|
||||
verify(exactly = 1) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sendData queues ATAK_PLUGIN when disconnected`() {
|
||||
val packet = DataPacket(dataType = PortNum.ATAK_PLUGIN.value, bytes = ByteString.EMPTY)
|
||||
commandSender.sendData(packet)
|
||||
|
||||
verify(exactly = 0) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
|
||||
connectionStateFlow.value = ConnectionState.Connected
|
||||
commandSender.processQueuedPackets()
|
||||
|
||||
verify(exactly = 1) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sendData queues ATAK_FORWARDER when disconnected`() {
|
||||
val packet = DataPacket(dataType = PortNum.ATAK_FORWARDER.value, bytes = ByteString.EMPTY)
|
||||
commandSender.sendData(packet)
|
||||
|
||||
verify(exactly = 0) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
|
||||
connectionStateFlow.value = ConnectionState.Connected
|
||||
commandSender.processQueuedPackets()
|
||||
|
||||
verify(exactly = 1) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sendData queues DETECTION_SENSOR_APP when disconnected`() {
|
||||
val packet = DataPacket(dataType = PortNum.DETECTION_SENSOR_APP.value, bytes = ByteString.EMPTY)
|
||||
commandSender.sendData(packet)
|
||||
|
||||
verify(exactly = 0) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
|
||||
connectionStateFlow.value = ConnectionState.Connected
|
||||
commandSender.processQueuedPackets()
|
||||
|
||||
verify(exactly = 1) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sendData queues PRIVATE_APP when disconnected`() {
|
||||
val packet = DataPacket(dataType = PortNum.PRIVATE_APP.value, bytes = ByteString.EMPTY)
|
||||
commandSender.sendData(packet)
|
||||
|
||||
verify(exactly = 0) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
|
||||
connectionStateFlow.value = ConnectionState.Connected
|
||||
commandSender.processQueuedPackets()
|
||||
|
||||
verify(exactly = 1) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sendData does NOT queue IP_TUNNEL_APP when disconnected`() {
|
||||
val packet = DataPacket(dataType = PortNum.IP_TUNNEL_APP.value, bytes = ByteString.EMPTY)
|
||||
commandSender.sendData(packet)
|
||||
|
||||
verify(exactly = 0) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
|
||||
connectionStateFlow.value = ConnectionState.Connected
|
||||
commandSender.processQueuedPackets()
|
||||
|
||||
verify(exactly = 0) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,10 @@ class MainActivity : ComponentActivity() {
|
||||
addAction("com.geeksville.mesh.RECEIVED.POSITION_APP")
|
||||
addAction("com.geeksville.mesh.RECEIVED.TELEMETRY_APP")
|
||||
addAction("com.geeksville.mesh.RECEIVED.NODEINFO_APP")
|
||||
addAction("com.geeksville.mesh.RECEIVED.ATAK_PLUGIN")
|
||||
addAction("com.geeksville.mesh.RECEIVED.ATAK_FORWARDER")
|
||||
addAction("com.geeksville.mesh.RECEIVED.DETECTION_SENSOR_APP")
|
||||
addAction("com.geeksville.mesh.RECEIVED.PRIVATE_APP")
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
|
||||
@@ -90,6 +90,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.model.NodeInfo
|
||||
import org.meshtastic.proto.PortNum
|
||||
|
||||
@Composable
|
||||
fun ListItem(
|
||||
@@ -229,6 +230,7 @@ private fun MainContent(
|
||||
) {
|
||||
item { MyInfoSection(myId, myNodeInfo) }
|
||||
item { TitledCard(title = "Messaging") { MessagingSection(viewModel, lastMessage) } }
|
||||
item { TitledCard(title = "Test Special PortNums") { SpecialAppSection(viewModel) } }
|
||||
|
||||
item {
|
||||
SectionHeader(
|
||||
@@ -297,6 +299,28 @@ private fun MainContent(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SpecialAppSection(viewModel: MeshServiceViewModel) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(onClick = { viewModel.sendSpecialPacket(PortNum.ATAK_PLUGIN) }, modifier = Modifier.weight(1f)) {
|
||||
Text("Send ATAK")
|
||||
}
|
||||
Button(
|
||||
onClick = { viewModel.sendSpecialPacket(PortNum.DETECTION_SENSOR_APP) },
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text("Send Sensor")
|
||||
}
|
||||
}
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(onClick = { viewModel.sendSpecialPacket(PortNum.PRIVATE_APP) }, modifier = Modifier.weight(1f)) {
|
||||
Text("Send Private")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PacketLogContent(log: List<String>) {
|
||||
Column(modifier = Modifier.fillMaxWidth().heightIn(max = 300.dp).verticalScroll(rememberScrollState())) {
|
||||
|
||||
@@ -135,6 +135,31 @@ class MeshServiceViewModel : ViewModel() {
|
||||
} ?: Log.w(TAG, "MeshService is not bound, cannot send message")
|
||||
}
|
||||
|
||||
fun sendSpecialPacket(portNum: PortNum) {
|
||||
meshService?.let { service ->
|
||||
try {
|
||||
val packet =
|
||||
DataPacket(
|
||||
to = DataPacket.ID_BROADCAST,
|
||||
bytes = "Special Payload for ${portNum.name}".encodeToByteArray().toByteString(),
|
||||
dataType = portNum.value,
|
||||
from = DataPacket.ID_LOCAL,
|
||||
time = System.currentTimeMillis(),
|
||||
id = service.packetId,
|
||||
status = MessageStatus.UNKNOWN,
|
||||
hopLimit = 3,
|
||||
channel = 0,
|
||||
wantAck = true,
|
||||
)
|
||||
service.send(packet)
|
||||
addToLog("Sent ${portNum.name} Packet (ID: ${packet.id})")
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "Failed to send special packet", e)
|
||||
addToLog("Failed to send ${portNum.name} packet: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun requestMyNodeInfo() {
|
||||
meshService?.let {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user