mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-06-26 06:25:24 -04:00
feat(settings): add remote "Set time" admin action (#5821)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1
.skills/compose-ui/strings-index.txt
generated
1
.skills/compose-ui/strings-index.txt
generated
@@ -1273,6 +1273,7 @@ serial_tx_pin
|
||||
server
|
||||
session_active
|
||||
session_refresh_required
|
||||
set_time
|
||||
set_up_connection
|
||||
set_your_region
|
||||
settings
|
||||
|
||||
@@ -75,6 +75,20 @@ constructor(
|
||||
return packetId
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs the node's real-time clock to the phone's current time.
|
||||
*
|
||||
* Lets a user correct a remote node whose RTC has drifted without being on-site or using the CLI.
|
||||
*
|
||||
* @param destNum The node number to update.
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
open suspend fun setTime(destNum: Int): Int {
|
||||
val packetId = radioController.generatePacketId()
|
||||
radioController.setTime(destNum, packetId)
|
||||
return packetId
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the NodeDB on the radio.
|
||||
*
|
||||
|
||||
@@ -83,6 +83,15 @@ interface AdminController {
|
||||
/** Updates the canned messages configuration on a remote node. */
|
||||
suspend fun setCannedMessages(destNum: Int, messages: String)
|
||||
|
||||
/**
|
||||
* Syncs a node's real-time clock to the phone's current time via `AdminMessage.set_time_only`.
|
||||
*
|
||||
* Mirrors the Python CLI's `Node.setTime`: an accurate epoch-seconds timestamp is sent so a remote node whose RTC
|
||||
* has drifted can be corrected without an on-site visit. Fire-and-forget — the firmware applies the value without
|
||||
* an admin response (the routing ACK confirms delivery).
|
||||
*/
|
||||
suspend fun setTime(destNum: Int, packetId: Int)
|
||||
|
||||
// ── Remote queries ──────────────────────────────────────────────────────
|
||||
|
||||
/** Requests the current owner (user info) from a remote node. */
|
||||
|
||||
@@ -1315,6 +1315,7 @@
|
||||
<string name="server">Server</string>
|
||||
<string name="session_active">Session active</string>
|
||||
<string name="session_refresh_required">Refresh required</string>
|
||||
<string name="set_time">Set time</string>
|
||||
<string name="set_up_connection">Set up connection</string>
|
||||
<string name="set_your_region">Set your region</string>
|
||||
<string name="settings">settings</string>
|
||||
|
||||
@@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.firstOrNull
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.common.util.handledLaunch
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.repository.AdminController
|
||||
import org.meshtastic.core.repository.AdminEditScope
|
||||
@@ -160,6 +161,12 @@ internal class AdminControllerImpl(
|
||||
commandSender.sendAdmin(destNum) { AdminMessage(set_canned_message_module_messages = messages) }
|
||||
}
|
||||
|
||||
override suspend fun setTime(destNum: Int, packetId: Int) {
|
||||
Logger.i { "Set time requested for node $destNum" }
|
||||
// Resolve the timestamp at send time so the value is as fresh as possible when it leaves the phone.
|
||||
commandSender.sendAdmin(destNum, packetId) { AdminMessage(set_time_only = nowSeconds.toInt()) }
|
||||
}
|
||||
|
||||
override suspend fun getCannedMessages(destNum: Int, packetId: Int) {
|
||||
commandSender.sendAdmin(destNum, packetId, wantResponse = true) {
|
||||
AdminMessage(get_canned_message_module_messages_request = true)
|
||||
|
||||
@@ -339,6 +339,25 @@ class RadioControllerImplTest {
|
||||
verifySuspend { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setTimeSendsAdminMessageWithCurrentEpochSeconds() = runTest {
|
||||
val controller = createController()
|
||||
|
||||
var sentMessage: AdminMessage? = null
|
||||
everySuspend { commandSender.sendAdmin(any(), any(), any(), any()) } calls
|
||||
{
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
sentMessage = (it.args[3] as () -> AdminMessage)()
|
||||
}
|
||||
|
||||
controller.setTime(destNum = 101, packetId = 11)
|
||||
|
||||
verifySuspend { commandSender.sendAdmin(any(), any(), any(), any()) }
|
||||
// The phone's current time is sent; assert it is populated and a plausible recent epoch (after 2020).
|
||||
val setTime = sentMessage?.set_time_only
|
||||
assertTrue(setTime != null && setTime > 1_577_836_800)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun refreshMetadataSendsAdminWithWantResponse() = runTest {
|
||||
val controller = createController()
|
||||
|
||||
@@ -123,6 +123,8 @@ class FakeRadioController :
|
||||
|
||||
override suspend fun setCannedMessages(destNum: Int, messages: String) {}
|
||||
|
||||
override suspend fun setTime(destNum: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun getOwner(destNum: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun getConfig(destNum: Int, configType: Int, packetId: Int) {}
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.meshtastic.core.resources.firmware_update_title
|
||||
import org.meshtastic.core.resources.ic_power_settings_new
|
||||
import org.meshtastic.core.resources.ic_restart_alt
|
||||
import org.meshtastic.core.resources.ic_restore
|
||||
import org.meshtastic.core.resources.ic_schedule
|
||||
import org.meshtastic.core.resources.ic_storage
|
||||
import org.meshtastic.core.resources.import_configuration
|
||||
import org.meshtastic.core.resources.message_device_managed
|
||||
@@ -51,6 +52,7 @@ import org.meshtastic.core.resources.module_settings
|
||||
import org.meshtastic.core.resources.nodedb_reset
|
||||
import org.meshtastic.core.resources.radio_configuration
|
||||
import org.meshtastic.core.resources.reboot
|
||||
import org.meshtastic.core.resources.set_time
|
||||
import org.meshtastic.core.resources.shutdown
|
||||
import org.meshtastic.core.resources.tak_server
|
||||
import org.meshtastic.core.ui.component.ListItem
|
||||
@@ -227,6 +229,7 @@ private fun AdvancedSection(isManaged: Boolean, isOtaCapable: Boolean, enabled:
|
||||
}
|
||||
|
||||
enum class AdminRoute(val icon: DrawableResource, val title: StringResource) {
|
||||
SET_TIME(Res.drawable.ic_schedule, Res.string.set_time),
|
||||
REBOOT(Res.drawable.ic_restart_alt, Res.string.reboot),
|
||||
SHUTDOWN(Res.drawable.ic_power_settings_new, Res.string.shutdown),
|
||||
FACTORY_RESET(Res.drawable.ic_restore, Res.string.factory_reset),
|
||||
|
||||
@@ -445,6 +445,12 @@ open class RadioConfigViewModel(
|
||||
val preserveFavorites = radioConfigState.value.nodeDbResetPreserveFavorites
|
||||
|
||||
when (route) {
|
||||
AdminRoute.SET_TIME.name ->
|
||||
safeLaunch(tag = "setTime") {
|
||||
val packetId = adminActionsUseCase.setTime(destNum)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
|
||||
AdminRoute.REBOOT.name ->
|
||||
safeLaunch(tag = "reboot") {
|
||||
val packetId = adminActionsUseCase.reboot(destNum)
|
||||
|
||||
Reference in New Issue
Block a user