Skip to content

Commit b708b2f

Browse files
committed
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.
1 parent fda96e2 commit b708b2f

6 files changed

Lines changed: 95 additions & 42 deletions

File tree

core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class FromRadioPacketHandlerImpl(
5959
val myInfo = proto.my_info
6060
val metadata = proto.metadata
6161
val nodeInfo = proto.node_info
62+
val nodeInfoBatch = proto.node_info_batch
6263
val configCompleteId = proto.config_complete_id
6364
val mqttProxyMessage = proto.mqttClientProxyMessage
6465
val queueStatus = proto.queueStatus
@@ -80,6 +81,10 @@ class FromRadioPacketHandlerImpl(
8081
router.value.configFlowManager.handleNodeInfo(nodeInfo)
8182
serviceRepository.setConnectionProgress("Nodes (${router.value.configFlowManager.newNodeCount})")
8283
}
84+
nodeInfoBatch != null -> {
85+
nodeInfoBatch.items.forEach { info -> router.value.configFlowManager.handleNodeInfo(info) }
86+
serviceRepository.setConnectionProgress("Nodes (${router.value.configFlowManager.newNodeCount})")
87+
}
8388
configCompleteId != null -> router.value.configFlowManager.handleConfigComplete(configCompleteId)
8489
mqttProxyMessage != null -> mqttManager.handleMqttProxyMessage(mqttProxyMessage)
8590
queueStatus != null -> packetHandler.handleQueueStatus(queueStatus)

core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ class MeshConfigFlowManagerImpl(
7777
override fun handleConfigComplete(configCompleteId: Int) {
7878
when (configCompleteId) {
7979
HandshakeConstants.CONFIG_NONCE -> handleConfigOnlyComplete()
80-
HandshakeConstants.NODE_INFO_NONCE -> handleNodeInfoComplete()
80+
HandshakeConstants.NODE_INFO_NONCE,
81+
HandshakeConstants.BATCH_NODE_INFO_NONCE,
82+
-> handleNodeInfoComplete()
8183
else -> Logger.w { "Config complete id mismatch: $configCompleteId" }
8284
}
8385
}
@@ -120,10 +122,11 @@ class MeshConfigFlowManagerImpl(
120122

121123
private fun handleNodeInfoComplete() {
122124
Logger.i { "NodeInfo complete (Stage 2)" }
123-
val entities = newNodes.map { info ->
124-
nodeManager.installNodeInfo(info, withBroadcast = false)
125-
nodeManager.nodeDBbyNodeNum[info.num]!!
126-
}
125+
val entities =
126+
newNodes.map { info ->
127+
nodeManager.installNodeInfo(info, withBroadcast = false)
128+
nodeManager.nodeDBbyNodeNum[info.num]!!
129+
}
127130
newNodes.clear()
128131

129132
scope.handledLaunch {

core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ class MeshConnectionManagerImpl(
263263
}
264264

265265
override fun startNodeInfoOnly() {
266-
val action = { packetHandler.sendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE)) }
266+
val action = { packetHandler.sendToRadio(ToRadio(want_config_id = HandshakeConstants.BATCH_NODE_INFO_NONCE)) }
267267
startHandshakeStallGuard(2, action)
268268
action()
269269
}

core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImplTest.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import org.meshtastic.proto.FromRadio
3636
import org.meshtastic.proto.ModuleConfig
3737
import org.meshtastic.proto.MqttClientProxyMessage
3838
import org.meshtastic.proto.MyNodeInfo
39+
import org.meshtastic.proto.NodeInfoBatch
3940
import org.meshtastic.proto.QueueStatus
4041
import kotlin.test.BeforeTest
4142
import kotlin.test.Test
@@ -161,6 +162,24 @@ class FromRadioPacketHandlerImplTest {
161162
verify { mqttManager.handleMqttProxyMessage(proxyMsg) }
162163
}
163164

165+
@Test
166+
fun `handleFromRadio routes NODE_INFO_BATCH items to configFlowManager and updates status`() {
167+
val node1 = ProtoNodeInfo(num = 1111)
168+
val node2 = ProtoNodeInfo(num = 2222)
169+
val node3 = ProtoNodeInfo(num = 3333)
170+
val batch = NodeInfoBatch(items = listOf(node1, node2, node3))
171+
val proto = FromRadio(node_info_batch = batch)
172+
173+
every { configFlowManager.newNodeCount } returns 3
174+
175+
handler.handleFromRadio(proto)
176+
177+
verify { configFlowManager.handleNodeInfo(node1) }
178+
verify { configFlowManager.handleNodeInfo(node2) }
179+
verify { configFlowManager.handleNodeInfo(node3) }
180+
verify { serviceRepository.setConnectionProgress("Nodes (3)") }
181+
}
182+
164183
@Test
165184
fun `handleFromRadio routes CLIENTNOTIFICATION to serviceRepository`() {
166185
val notification = ClientNotification(message = "test")

core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockInterface.kt

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import org.meshtastic.core.common.util.nowSeconds
2525
import org.meshtastic.core.model.Channel
2626
import org.meshtastic.core.model.DataPacket
2727
import org.meshtastic.core.model.util.getInitials
28+
import org.meshtastic.core.repository.HandshakeConstants
2829
import org.meshtastic.core.repository.RadioInterfaceService
2930
import org.meshtastic.core.repository.RadioTransport
3031
import org.meshtastic.proto.AdminMessage
@@ -39,6 +40,7 @@ import org.meshtastic.proto.ModuleConfig
3940
import org.meshtastic.proto.Neighbor
4041
import org.meshtastic.proto.NeighborInfo
4142
import org.meshtastic.proto.NodeInfo
43+
import org.meshtastic.proto.NodeInfoBatch
4244
import org.meshtastic.proto.PortNum
4345
import org.meshtastic.proto.QueueStatus
4446
import org.meshtastic.proto.Routing
@@ -301,58 +303,79 @@ class MockInterface(private val service: RadioInterfaceService, val address: Str
301303
service.handleFromRadio(makeAck(MY_NODE + 1, packet.from, packet.id).encode())
302304
}
303305

304-
private fun sendConfigResponse(configId: Int) {
305-
Logger.d { "Sending mock config response" }
306+
// / Generate a fake NodeInfo for a simulated node
307+
@Suppress("MagicNumber")
308+
private fun makeSimNodeInfo(numIn: Int, lat: Double, lon: Double) = NodeInfo(
309+
num = numIn,
310+
user =
311+
User(
312+
id = DataPacket.nodeNumToDefaultId(numIn),
313+
long_name = "Sim " + numIn.toString(16),
314+
short_name = getInitials("Sim " + numIn.toString(16)),
315+
hw_model = HardwareModel.ANDROID_SIM,
316+
),
317+
position =
318+
ProtoPosition(
319+
latitude_i = org.meshtastic.core.model.Position.degI(lat),
320+
longitude_i = org.meshtastic.core.model.Position.degI(lon),
321+
altitude = 35,
322+
time = nowSeconds.toInt(),
323+
precision_bits = Random.nextInt(10, 19),
324+
),
325+
)
306326

307-
// / Generate a fake node info entry
308-
@Suppress("MagicNumber")
309-
fun makeNodeInfo(numIn: Int, lat: Double, lon: Double) = FromRadio(
310-
node_info =
311-
NodeInfo(
312-
num = numIn,
313-
user =
314-
User(
315-
id = DataPacket.nodeNumToDefaultId(numIn),
316-
long_name = "Sim " + numIn.toString(16),
317-
short_name = getInitials("Sim " + numIn.toString(16)),
318-
hw_model = HardwareModel.ANDROID_SIM,
319-
),
320-
position =
321-
ProtoPosition(
322-
latitude_i = org.meshtastic.core.model.Position.degI(lat),
323-
longitude_i = org.meshtastic.core.model.Position.degI(lon),
324-
altitude = 35,
325-
time = nowSeconds.toInt(),
326-
precision_bits = Random.nextInt(10, 19),
327-
),
328-
),
329-
)
327+
private fun sendConfigResponse(configId: Int) {
328+
Logger.d { "Sending mock config response for nonce=$configId" }
329+
when (configId) {
330+
HandshakeConstants.CONFIG_NONCE -> sendStage1ConfigResponse(configId)
331+
HandshakeConstants.BATCH_NODE_INFO_NONCE,
332+
HandshakeConstants.NODE_INFO_NONCE,
333+
-> sendStage2NodeInfoResponse(configId)
334+
else -> Logger.w { "Unknown config nonce $configId — ignoring" }
335+
}
336+
}
330337

331-
// Simulated network data to feed to our app
338+
/** Stage 1: send my_info, metadata, config, channels, then config_complete_id. No nodes. */
339+
private fun sendStage1ConfigResponse(configId: Int) {
332340
val packets =
333341
arrayOf(
334-
// MyNodeInfo
335342
FromRadio(my_info = ProtoMyNodeInfo(my_node_num = MY_NODE)),
336343
FromRadio(
337344
metadata = DeviceMetadata(firmware_version = "9.9.9.abcdefg", hw_model = HardwareModel.ANDROID_SIM),
338345
),
339-
340-
// Fake NodeDB
341-
makeNodeInfo(MY_NODE, 32.776665, -96.796989), // dallas
342-
makeNodeInfo(MY_NODE + 1, 32.960758, -96.733521), // richardson
343346
FromRadio(config = Config(lora = defaultLoRaConfig)),
344347
FromRadio(config = Config(lora = defaultLoRaConfig)),
345348
FromRadio(channel = defaultChannel),
346349
FromRadio(config_complete_id = configId),
350+
)
351+
packets.forEach { p -> service.handleFromRadio(p.encode()) }
352+
}
347353

348-
// Done with config response, now pretend to receive some text messages
354+
/**
355+
* Stage 2: send all nodes as a single [NodeInfoBatch], then config_complete_id. After the handshake completes,
356+
* simulate live traffic.
357+
*/
358+
private fun sendStage2NodeInfoResponse(configId: Int) {
359+
val batch =
360+
NodeInfoBatch(
361+
items =
362+
listOf(
363+
makeSimNodeInfo(MY_NODE, 32.776665, -96.796989), // dallas
364+
makeSimNodeInfo(MY_NODE + 1, 32.960758, -96.733521), // richardson
365+
),
366+
)
367+
val packets =
368+
arrayOf(
369+
FromRadio(node_info_batch = batch),
370+
FromRadio(config_complete_id = configId),
371+
372+
// Simulate live traffic after handshake
349373
makeTextMessage(MY_NODE + 1),
350374
makeNeighborInfo(MY_NODE + 1),
351375
makePosition(MY_NODE + 1),
352376
makeTelemetry(MY_NODE + 1),
353377
makeNodeStatus(MY_NODE + 1),
354378
)
355-
356379
packets.forEach { p -> service.handleFromRadio(p.encode()) }
357380
}
358381
}

core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HandshakeConstants.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,18 @@ package org.meshtastic.core.repository
1919
/**
2020
* Shared constants for the two-stage mesh handshake protocol.
2121
*
22-
* Stage 1 (`CONFIG_NONCE`): requests device config, module config, and channels. Stage 2 (`NODE_INFO_NONCE`): requests
23-
* the full node database.
22+
* Stage 1 (`CONFIG_NONCE`): requests device config, module config, and channels. Stage 2 (`BATCH_NODE_INFO_NONCE`):
23+
* requests the full node database with batched NodeInfo delivery.
2424
*
2525
* Both [MeshConfigFlowManager] (consumer) and [MeshConnectionManager] (sender) reference these.
2626
*/
2727
object HandshakeConstants {
2828
/** Nonce sent in `want_config_id` to request config-only (Stage 1). */
2929
const val CONFIG_NONCE = 69420
3030

31-
/** Nonce sent in `want_config_id` to request node info only (Stage 2). */
31+
/** Nonce sent in `want_config_id` to request node info only — unbatched legacy (Stage 2). */
3232
const val NODE_INFO_NONCE = 69421
33+
34+
/** Nonce sent in `want_config_id` to request node info only — batched (Stage 2). */
35+
const val BATCH_NODE_INFO_NONCE = 69423
3336
}

0 commit comments

Comments
 (0)