diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt index 9a84026fa..da7a9e2a0 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt @@ -59,6 +59,7 @@ class FromRadioPacketHandlerImpl( val myInfo = proto.my_info val metadata = proto.metadata val nodeInfo = proto.node_info + val nodeInfoBatch = proto.node_info_batch val configCompleteId = proto.config_complete_id val mqttProxyMessage = proto.mqttClientProxyMessage val queueStatus = proto.queueStatus @@ -80,6 +81,10 @@ class FromRadioPacketHandlerImpl( router.value.configFlowManager.handleNodeInfo(nodeInfo) serviceRepository.setConnectionProgress("Nodes (${router.value.configFlowManager.newNodeCount})") } + nodeInfoBatch != null -> { + nodeInfoBatch.items.forEach { info -> router.value.configFlowManager.handleNodeInfo(info) } + serviceRepository.setConnectionProgress("Nodes (${router.value.configFlowManager.newNodeCount})") + } configCompleteId != null -> router.value.configFlowManager.handleConfigComplete(configCompleteId) mqttProxyMessage != null -> mqttManager.handleMqttProxyMessage(mqttProxyMessage) queueStatus != null -> packetHandler.handleQueueStatus(queueStatus) diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt index 2e880bb3b..6a8bd5e40 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt @@ -77,7 +77,9 @@ class MeshConfigFlowManagerImpl( override fun handleConfigComplete(configCompleteId: Int) { when (configCompleteId) { HandshakeConstants.CONFIG_NONCE -> handleConfigOnlyComplete() - HandshakeConstants.NODE_INFO_NONCE -> handleNodeInfoComplete() + HandshakeConstants.NODE_INFO_NONCE, + HandshakeConstants.BATCH_NODE_INFO_NONCE, + -> handleNodeInfoComplete() else -> Logger.w { "Config complete id mismatch: $configCompleteId" } } } @@ -120,10 +122,11 @@ class MeshConfigFlowManagerImpl( private fun handleNodeInfoComplete() { Logger.i { "NodeInfo complete (Stage 2)" } - val entities = newNodes.map { info -> - nodeManager.installNodeInfo(info, withBroadcast = false) - nodeManager.nodeDBbyNodeNum[info.num]!! - } + val entities = + newNodes.map { info -> + nodeManager.installNodeInfo(info, withBroadcast = false) + nodeManager.nodeDBbyNodeNum[info.num]!! + } newNodes.clear() scope.handledLaunch { diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt index bd0cafa4c..cce43898f 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt @@ -263,7 +263,7 @@ class MeshConnectionManagerImpl( } override fun startNodeInfoOnly() { - val action = { packetHandler.sendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE)) } + val action = { packetHandler.sendToRadio(ToRadio(want_config_id = HandshakeConstants.BATCH_NODE_INFO_NONCE)) } startHandshakeStallGuard(2, action) action() } diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt index d3f0efc32..c4fc861b3 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt @@ -36,6 +36,7 @@ import org.meshtastic.proto.FromRadio import org.meshtastic.proto.ModuleConfig import org.meshtastic.proto.MqttClientProxyMessage import org.meshtastic.proto.MyNodeInfo +import org.meshtastic.proto.NodeInfoBatch import org.meshtastic.proto.QueueStatus import kotlin.test.BeforeTest import kotlin.test.Test @@ -161,6 +162,24 @@ class FromRadioPacketHandlerImplTest { verify { mqttManager.handleMqttProxyMessage(proxyMsg) } } + @Test + fun `handleFromRadio routes NODE_INFO_BATCH items to configFlowManager and updates status`() { + val node1 = ProtoNodeInfo(num = 1111) + val node2 = ProtoNodeInfo(num = 2222) + val node3 = ProtoNodeInfo(num = 3333) + val batch = NodeInfoBatch(items = listOf(node1, node2, node3)) + val proto = FromRadio(node_info_batch = batch) + + every { configFlowManager.newNodeCount } returns 3 + + handler.handleFromRadio(proto) + + verify { configFlowManager.handleNodeInfo(node1) } + verify { configFlowManager.handleNodeInfo(node2) } + verify { configFlowManager.handleNodeInfo(node3) } + verify { serviceRepository.setConnectionProgress("Nodes (3)") } + } + @Test fun `handleFromRadio routes CLIENTNOTIFICATION to serviceRepository`() { val notification = ClientNotification(message = "test") diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockInterface.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockInterface.kt index 4990ee7ab..7e9c7e991 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockInterface.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockInterface.kt @@ -25,6 +25,7 @@ import org.meshtastic.core.common.util.nowSeconds import org.meshtastic.core.model.Channel import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.util.getInitials +import org.meshtastic.core.repository.HandshakeConstants import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.RadioTransport import org.meshtastic.proto.AdminMessage @@ -39,6 +40,7 @@ import org.meshtastic.proto.ModuleConfig import org.meshtastic.proto.Neighbor import org.meshtastic.proto.NeighborInfo import org.meshtastic.proto.NodeInfo +import org.meshtastic.proto.NodeInfoBatch import org.meshtastic.proto.PortNum import org.meshtastic.proto.QueueStatus import org.meshtastic.proto.Routing @@ -301,58 +303,79 @@ class MockInterface(private val service: RadioInterfaceService, val address: Str service.handleFromRadio(makeAck(MY_NODE + 1, packet.from, packet.id).encode()) } + // / Generate a fake NodeInfo for a simulated node + @Suppress("MagicNumber") + private fun makeSimNodeInfo(numIn: Int, lat: Double, lon: Double) = NodeInfo( + num = numIn, + user = + User( + id = DataPacket.nodeNumToDefaultId(numIn), + long_name = "Sim " + numIn.toString(16), + short_name = getInitials("Sim " + numIn.toString(16)), + hw_model = HardwareModel.ANDROID_SIM, + ), + position = + ProtoPosition( + latitude_i = org.meshtastic.core.model.Position.degI(lat), + longitude_i = org.meshtastic.core.model.Position.degI(lon), + altitude = 35, + time = nowSeconds.toInt(), + precision_bits = Random.nextInt(10, 19), + ), + ) + private fun sendConfigResponse(configId: Int) { - Logger.d { "Sending mock config response" } + Logger.d { "Sending mock config response for nonce=$configId" } + when (configId) { + HandshakeConstants.CONFIG_NONCE -> sendStage1ConfigResponse(configId) + HandshakeConstants.BATCH_NODE_INFO_NONCE, + HandshakeConstants.NODE_INFO_NONCE, + -> sendStage2NodeInfoResponse(configId) + else -> Logger.w { "Unknown config nonce $configId — ignoring" } + } + } - // / Generate a fake node info entry - @Suppress("MagicNumber") - fun makeNodeInfo(numIn: Int, lat: Double, lon: Double) = FromRadio( - node_info = - NodeInfo( - num = numIn, - user = - User( - id = DataPacket.nodeNumToDefaultId(numIn), - long_name = "Sim " + numIn.toString(16), - short_name = getInitials("Sim " + numIn.toString(16)), - hw_model = HardwareModel.ANDROID_SIM, - ), - position = - ProtoPosition( - latitude_i = org.meshtastic.core.model.Position.degI(lat), - longitude_i = org.meshtastic.core.model.Position.degI(lon), - altitude = 35, - time = nowSeconds.toInt(), - precision_bits = Random.nextInt(10, 19), - ), - ), - ) - - // Simulated network data to feed to our app + /** Stage 1: send my_info, metadata, config, channels, then config_complete_id. No nodes. */ + private fun sendStage1ConfigResponse(configId: Int) { val packets = arrayOf( - // MyNodeInfo FromRadio(my_info = ProtoMyNodeInfo(my_node_num = MY_NODE)), FromRadio( metadata = DeviceMetadata(firmware_version = "9.9.9.abcdefg", hw_model = HardwareModel.ANDROID_SIM), ), - - // Fake NodeDB - makeNodeInfo(MY_NODE, 32.776665, -96.796989), // dallas - makeNodeInfo(MY_NODE + 1, 32.960758, -96.733521), // richardson FromRadio(config = Config(lora = defaultLoRaConfig)), FromRadio(config = Config(lora = defaultLoRaConfig)), FromRadio(channel = defaultChannel), FromRadio(config_complete_id = configId), + ) + packets.forEach { p -> service.handleFromRadio(p.encode()) } + } - // Done with config response, now pretend to receive some text messages + /** + * Stage 2: send all nodes as a single [NodeInfoBatch], then config_complete_id. After the handshake completes, + * simulate live traffic. + */ + private fun sendStage2NodeInfoResponse(configId: Int) { + val batch = + NodeInfoBatch( + items = + listOf( + makeSimNodeInfo(MY_NODE, 32.776665, -96.796989), // dallas + makeSimNodeInfo(MY_NODE + 1, 32.960758, -96.733521), // richardson + ), + ) + val packets = + arrayOf( + FromRadio(node_info_batch = batch), + FromRadio(config_complete_id = configId), + + // Simulate live traffic after handshake makeTextMessage(MY_NODE + 1), makeNeighborInfo(MY_NODE + 1), makePosition(MY_NODE + 1), makeTelemetry(MY_NODE + 1), makeNodeStatus(MY_NODE + 1), ) - packets.forEach { p -> service.handleFromRadio(p.encode()) } } } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HandshakeConstants.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HandshakeConstants.kt index 7b403aa36..06c68caf2 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HandshakeConstants.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HandshakeConstants.kt @@ -19,8 +19,8 @@ package org.meshtastic.core.repository /** * Shared constants for the two-stage mesh handshake protocol. * - * Stage 1 (`CONFIG_NONCE`): requests device config, module config, and channels. Stage 2 (`NODE_INFO_NONCE`): requests - * the full node database. + * Stage 1 (`CONFIG_NONCE`): requests device config, module config, and channels. Stage 2 (`BATCH_NODE_INFO_NONCE`): + * requests the full node database with batched NodeInfo delivery. * * Both [MeshConfigFlowManager] (consumer) and [MeshConnectionManager] (sender) reference these. */ @@ -28,6 +28,9 @@ object HandshakeConstants { /** Nonce sent in `want_config_id` to request config-only (Stage 1). */ const val CONFIG_NONCE = 69420 - /** Nonce sent in `want_config_id` to request node info only (Stage 2). */ + /** Nonce sent in `want_config_id` to request node info only — unbatched legacy (Stage 2). */ const val NODE_INFO_NONCE = 69421 + + /** Nonce sent in `want_config_id` to request node info only — batched (Stage 2). */ + const val BATCH_NODE_INFO_NONCE = 69423 }