feat(core): implement batched node info handling for mesh handshake

This commit optimizes the mesh handshake protocol by introducing explicit support for `NodeInfoBatch` messages. It updates the configuration flow to handle both primary batched node delivery and legacy single-node delivery for backwards compatibility with older firmware.

Key changes include:

- **Batch Processing Optimization:**
    - Added `handleNodeInfoBatch` to the `MeshConfigFlowManager` interface to allow bulk processing of node information, reducing per-item overhead during the initial handshake.
    - Updated `MeshConfigFlowManagerImpl` to accumulate batched nodes efficiently using `addAll`.
    - Refactored `FromRadioPacketHandlerImpl` to delegate batch processing directly to the manager instead of iterating through individual items.

- **Handshake Protocol Updates:**
    - Updated `HandshakeConstants` to distinguish between `BATCH_NODE_INFO_NONCE` (primary Stage 2) and `NODE_INFO_NONCE` (legacy Stage 2).
    - Modified `handleConfigComplete` logic to trigger Stage 2 completion for both batched and legacy nonces.
    - Ensured `MeshConnectionManager` prioritizes the batch nonce when requesting node information.

- **Testing & Simulation:**
    - Created `MeshConfigFlowManagerImplTest` to validate node accumulation, batch handling, and handshake nonce routing.
    - Improved `MockInterface` to better simulate real-world packet ordering by delaying live traffic until after the handshake completion coroutine has processed the node database.
    - Added verification tests to ensure the connection manager uses the correct batching nonces.

Specific changes:
- Added `handleNodeInfoBatch` implementation to `MeshConfigFlowManagerImpl`.
- Updated documentation in `HandshakeConstants` regarding two-stage mesh handshake protocol.
- Refactored `MockInterface.sendStage2NodeInfoResponse` to handle packet encoding and simulation delays.
- Added unit tests covering edge cases for empty and mixed node info batches.
This commit is contained in:
James Rich 2026-04-03 10:36:16 -05:00
parent b708b2ff76
commit a74d5d470a
8 changed files with 271 additions and 30 deletions

View file

@ -352,8 +352,9 @@ class MockInterface(private val service: RadioInterfaceService, val address: Str
}
/**
* Stage 2: send all nodes as a single [NodeInfoBatch], then config_complete_id. After the handshake completes,
* simulate live traffic.
* Stage 2: send all nodes as a single [NodeInfoBatch], then config_complete_id. Live traffic is simulated in a
* separate coroutine after a short delay so it arrives *after* the handshake completion coroutine has had a chance
* to commit the node DB matching real-world ordering.
*/
private fun sendStage2NodeInfoResponse(configId: Int) {
val batch =
@ -364,18 +365,22 @@ class MockInterface(private val service: RadioInterfaceService, val address: Str
makeSimNodeInfo(MY_NODE + 1, 32.960758, -96.733521), // richardson
),
)
val packets =
arrayOf(
FromRadio(node_info_batch = batch),
FromRadio(config_complete_id = configId),
listOf(FromRadio(node_info_batch = batch), FromRadio(config_complete_id = configId)).forEach { p ->
service.handleFromRadio(p.encode())
}
// Simulate live traffic after handshake
// Simulate live traffic after the handshake has completed. Launched in a separate
// coroutine with a small delay so these packets arrive after onNodeDbReady() runs.
service.serviceScope.handledLaunch {
delay(200)
listOf(
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()) }
.forEach { p -> service.handleFromRadio(p.encode()) }
}
}
}