feat: implement batched NodeInfo delivery in handshake protocol

This commit introduces support for `NodeInfoBatch` messages, allowing the application to receive and process multiple node records efficiently during the second stage of the device handshake. This replaces the legacy approach of sending node information individually, improving synchronization performance.

Key changes include:

- **Handshake Protocol:**
    - Added `BATCH_NODE_INFO_NONCE` to `HandshakeConstants`.
    - Updated `MeshConnectionManagerImpl` to request batched node information by default during the handshake process.
    - Updated `MeshConfigFlowManagerImpl` to handle the batch-specific completion nonce, ensuring Stage 2 of the handshake finalizes correctly.

- **Packet Handling:**
    - Enhanced `FromRadioPacketHandlerImpl` to detect `node_info_batch` packets.
    - Implemented logic to iterate through batched items and process each `NodeInfo` record via the `MeshConfigFlowManager`.
    - Updated connection progress reporting to reflect the count of nodes received within a batch.

- **Testing and Simulation:**
    - Updated `MockInterface` to simulate batched node delivery, refactoring the mock response logic into distinct Stage 1 (config) and Stage 2 (node info) phases.
    - Added a unit test in `FromRadioPacketHandlerImplTest` to verify that batched items are correctly routed and that the UI connection status is updated.

Specific changes:
- Modified `HandshakeConstants` to document the transition to batched NodeInfo delivery in Stage 2.
- Updated `MeshConnectionManagerImpl.startNodeInfoOnly()` to use the new batch nonce.
- Added `nodeInfoBatch` processing branch to the `handleFromRadio` logic.
This commit is contained in:
James Rich 2026-04-02 15:35:06 -05:00
parent fda96e2f8c
commit b708b2ff76
6 changed files with 95 additions and 42 deletions

View file

@ -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)

View file

@ -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 {

View file

@ -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()
}

View file

@ -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")