mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-09-22 11:45:07 -04:00
fix(connections): send set_time_only at MyNodeInfo instead of onNodeDbReady (#6503)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
39139398e6
commit
8a4e2e3567
8 files changed
+71
-7
No files matched your search
@@ -182,6 +182,12 @@ class CommandSenderImpl(
|
||||
packetHandler.sendToRadio(packet)
|
||||
}
|
||||
|
||||
override fun sendAdminImmediate(destNum: Int, initFn: () -> AdminMessage) {
|
||||
val adminMsg = initFn().copy(session_passkey = sessionManager.getPasskey(destNum))
|
||||
val packet = buildAdminPacket(to = destNum, adminMessage = adminMsg)
|
||||
packetHandler.sendToRadio(ToRadio(packet = packet))
|
||||
}
|
||||
|
||||
override suspend fun sendAdminAwait(
|
||||
destNum: Int,
|
||||
requestId: Int,
|
||||
|
||||
+1
@@ -343,6 +343,7 @@ class MeshConfigFlowManagerImpl(
|
||||
// this newer session. The async clear also rechecks transport authority before touching persistence.
|
||||
clearGeneration = handshakeGeneration.incrementAndGet()
|
||||
connectionManager.value.onHandshakeProgress()
|
||||
connectionManager.value.onMyNodeInfoReceived(myInfo.my_node_num)
|
||||
}
|
||||
if (!admitted) {
|
||||
Logger.d { "[DeviceAssociation] discard stale MyNodeInfo gen=${session.generation}" }
|
||||
|
||||
+13
-7
@@ -500,20 +500,26 @@ class MeshConnectionManagerImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMyNodeInfoReceived(myNodeNum: Int) {
|
||||
// Set device time as early as possible: MyNodeInfo is the first Stage 1 frame, so this
|
||||
// lands before the firmware flushes its queued packet backlog and lets it stamp those
|
||||
// packets with a corrected clock (firmware #11274). A single small write ahead of the
|
||||
// config/node-info bursts avoids the GATT contention that pushed the old
|
||||
// onRadioConfigLoaded-time send out of Stage 1. Must bypass the outbound packet queue:
|
||||
// it only drains once Connected, which would hold this until after the backlog flush.
|
||||
commandSender.sendAdminImmediate(myNodeNum) { AdminMessage(set_time_only = nowSeconds.toInt()) }
|
||||
}
|
||||
|
||||
override suspend fun onNodeDbReady() {
|
||||
// Collapse cancel+clear into one atomic swap so a concurrent re-arm cannot
|
||||
// orphan a job in the gap between cancel and reassign.
|
||||
handshakeTimeout.getAndSet(null)?.cancel()
|
||||
|
||||
val myNodeNum = nodeManager.myNodeNum.value ?: 0
|
||||
// Set device time now that the full node picture is ready. Sending this during Stage 1
|
||||
// (onRadioConfigLoaded) introduced GATT write contention with the Stage 2 node-info burst.
|
||||
commandSender.sendAdmin(myNodeNum) { AdminMessage(set_time_only = nowSeconds.toInt()) }
|
||||
|
||||
// Proactively seed the session passkey. The firmware embeds session_passkey in every
|
||||
// admin *response* (wantResponse=true), but set_time_only has no response. A get_owner
|
||||
// request is the lightest way to trigger a response and populate the passkey cache so
|
||||
// that subsequent write operations don't fail with ADMIN_BAD_SESSION_KEY.
|
||||
// admin *response* (wantResponse=true), but set_time_only (sent at MyNodeInfo) has no
|
||||
// response. A get_owner request is the lightest way to trigger a response and populate the
|
||||
// passkey cache so that subsequent write operations don't fail with ADMIN_BAD_SESSION_KEY.
|
||||
commandSender.sendAdmin(myNodeNum, wantResponse = true) { AdminMessage(get_owner_request = true) }
|
||||
|
||||
// Start MQTT if enabled
|
||||
|
||||
+31
@@ -21,6 +21,7 @@ import dev.mokkery.answering.returns
|
||||
import dev.mokkery.every
|
||||
import dev.mokkery.everySuspend
|
||||
import dev.mokkery.matcher.any
|
||||
import dev.mokkery.matcher.matches
|
||||
import dev.mokkery.mock
|
||||
import dev.mokkery.verify
|
||||
import dev.mokkery.verifySuspend
|
||||
@@ -40,11 +41,13 @@ import org.meshtastic.core.repository.PacketHandler
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.SessionManager
|
||||
import org.meshtastic.core.repository.TracerouteHandler
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.NeighborInfo
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.ToRadio
|
||||
import org.meshtastic.proto.User
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
@@ -202,6 +205,34 @@ class CommandSenderImplTest {
|
||||
verifySuspend { packetHandler.sendToRadio(any<MeshPacket>()) }
|
||||
}
|
||||
|
||||
// --- sendAdminImmediate ---
|
||||
|
||||
@Test
|
||||
fun sendAdminImmediate_dispatchesDirectToRadioWithPasskeyAndNoResponse() {
|
||||
val passkey = "secret".encodeUtf8()
|
||||
every { sessionManager.getPasskey(DEST_NODE) } returns passkey
|
||||
every { packetHandler.sendToRadio(any<ToRadio>()) } returns Unit
|
||||
|
||||
commandSender.sendAdminImmediate(DEST_NODE) { AdminMessage(set_time_only = 12345) }
|
||||
|
||||
// Direct ToRadio dispatch (not the Connected-gated MeshPacket queue), correct destination,
|
||||
// no want_response, and the session passkey injected into the admin payload.
|
||||
verify {
|
||||
packetHandler.sendToRadio(
|
||||
matches<ToRadio> { toRadio ->
|
||||
val packet = toRadio.packet ?: return@matches false
|
||||
val decoded = packet.decoded ?: return@matches false
|
||||
val admin = AdminMessage.ADAPTER.decode(decoded.payload)
|
||||
packet.to == DEST_NODE &&
|
||||
decoded.portnum == PortNum.ADMIN_APP &&
|
||||
!decoded.want_response &&
|
||||
admin.set_time_only == 12345 &&
|
||||
admin.session_passkey == passkey
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- requestTraceroute ---
|
||||
|
||||
@Test
|
||||
|
||||
+4
@@ -114,6 +114,8 @@ class LockdownCoordinatorImplTest {
|
||||
initFn: () -> AdminMessage,
|
||||
) = Unit
|
||||
|
||||
override fun sendAdminImmediate(destNum: Int, initFn: () -> AdminMessage) = Unit
|
||||
|
||||
override suspend fun sendAdminAwait(
|
||||
destNum: Int,
|
||||
requestId: Int,
|
||||
@@ -149,6 +151,8 @@ class LockdownCoordinatorImplTest {
|
||||
|
||||
override fun startNodeInfoOnly() = Unit
|
||||
|
||||
override fun onMyNodeInfoReceived(myNodeNum: Int) = Unit
|
||||
|
||||
override suspend fun onNodeDbReady() = Unit
|
||||
|
||||
override fun updateTelemetry(t: Telemetry) = Unit
|
||||
|
||||
@@ -48,6 +48,13 @@ interface CommandSender {
|
||||
initFn: () -> AdminMessage,
|
||||
)
|
||||
|
||||
/**
|
||||
* Sends an admin message immediately, bypassing the outbound packet queue. The queue only drains while the
|
||||
* connection state is Connected, so mid-handshake sends (e.g. set_time_only at MyNodeInfo) must use this path or
|
||||
* they stall until the handshake finishes.
|
||||
*/
|
||||
fun sendAdminImmediate(destNum: Int, initFn: () -> AdminMessage)
|
||||
|
||||
/**
|
||||
* Sends an admin message and suspends until the radio acknowledges it.
|
||||
*
|
||||
|
||||
+7
@@ -29,6 +29,13 @@ interface MeshConnectionManager {
|
||||
/** Initiates the node information synchronization stage. */
|
||||
fun startNodeInfoOnly()
|
||||
|
||||
/**
|
||||
* Called when the MyNodeInfo frame arrives — the first frame of the Stage 1 config stream. This is the earliest
|
||||
* point where the local node number is authoritative, before the firmware flushes its queued packet backlog, so
|
||||
* time-sensitive setup (e.g. set_time_only) sent here lets the firmware stamp backlog packets with real rx_time.
|
||||
*/
|
||||
fun onMyNodeInfoReceived(myNodeNum: Int)
|
||||
|
||||
/** Called when the node database is ready and fully populated. */
|
||||
suspend fun onNodeDbReady()
|
||||
|
||||
|
||||
+2
@@ -134,6 +134,8 @@ class TAKMeshIntegrationTest {
|
||||
initFn: () -> AdminMessage,
|
||||
) {}
|
||||
|
||||
override fun sendAdminImmediate(destNum: Int, initFn: () -> AdminMessage) {}
|
||||
|
||||
override suspend fun sendAdminAwait(
|
||||
destNum: Int,
|
||||
requestId: Int,
|
||||
|
||||
Reference in new issue
Block a user