refactor(time): Centralize time handling with kotlinx-datetime (#4545)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-02-13 20:01:07 -06:00 committed by GitHub
parent da04448dee
commit 5ca2ab4695
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
86 changed files with 993 additions and 663 deletions

View file

@ -35,8 +35,9 @@ import kotlinx.coroutines.launch
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.prefs.mesh.MeshPrefs
import org.meshtastic.core.prefs.meshlog.MeshLogPrefs
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.time.Duration.Companion.hours
import kotlin.time.toJavaDuration
/**
* The main application class for Meshtastic.
@ -67,11 +68,7 @@ class MeshUtilApplication :
private fun scheduleMeshLogCleanup() {
val cleanupRequest =
PeriodicWorkRequestBuilder<MeshLogCleanupWorker>(
repeatInterval = 1,
repeatIntervalTimeUnit = TimeUnit.HOURS,
)
.build()
PeriodicWorkRequestBuilder<MeshLogCleanupWorker>(repeatInterval = 1.hours.toJavaDuration()).build()
WorkManager.getInstance(this)
.enqueueUniquePeriodicWork(

View file

@ -15,6 +15,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.concurrent
import org.meshtastic.core.model.util.nowMillis
/** A deferred execution object (with various possible implementations) */
interface Continuation<in T> {
abstract fun resume(res: Result<T>)
@ -63,10 +66,10 @@ class SyncContinuation<T> : Continuation<T> {
fun await(timeoutMsecs: Long = 0): T {
lock.lock()
try {
val startT = System.currentTimeMillis()
val startT = nowMillis
while (result == null) {
if (timeoutMsecs > 0) {
val remaining = timeoutMsecs - (System.currentTimeMillis() - startT)
val remaining = timeoutMsecs - (nowMillis - startT)
if (remaining <= 0) {
throw Exception("SyncContinuation timeout")
}

View file

@ -26,6 +26,7 @@ import okio.ByteString.Companion.encodeUtf8
import okio.ByteString.Companion.toByteString
import org.meshtastic.core.model.Channel
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.util.nowSeconds
import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.Config
import org.meshtastic.proto.Data
@ -146,7 +147,7 @@ constructor(
id = packetIdSequence.next(),
from = numIn,
to = 0xffffffff.toInt(), // broadcast
rx_time = (System.currentTimeMillis() / 1000).toInt(),
rx_time = nowSeconds.toInt(),
rx_snr = 1.5f,
decoded =
Data(
@ -162,7 +163,7 @@ constructor(
id = packetIdSequence.next(),
from = numIn,
to = 0xffffffff.toInt(), // broadcast
rx_time = (System.currentTimeMillis() / 1000).toInt(),
rx_time = nowSeconds.toInt(),
rx_snr = 1.5f,
decoded =
Data(
@ -177,13 +178,13 @@ constructor(
Neighbor(
node_id = numIn + 1,
snr = 10.0f,
last_rx_time = (System.currentTimeMillis() / 1000).toInt(),
last_rx_time = nowSeconds.toInt(),
node_broadcast_interval_secs = 60,
),
Neighbor(
node_id = numIn + 2,
snr = 12.0f,
last_rx_time = (System.currentTimeMillis() / 1000).toInt(),
last_rx_time = nowSeconds.toInt(),
node_broadcast_interval_secs = 60,
),
),
@ -200,7 +201,7 @@ constructor(
id = packetIdSequence.next(),
from = numIn,
to = 0xffffffff.toInt(), // broadcast
rx_time = (System.currentTimeMillis() / 1000).toInt(),
rx_time = nowSeconds.toInt(),
rx_snr = 1.5f,
decoded =
Data(
@ -210,7 +211,7 @@ constructor(
latitude_i = org.meshtastic.core.model.Position.degI(32.776665),
longitude_i = org.meshtastic.core.model.Position.degI(-96.796989),
altitude = 150,
time = (System.currentTimeMillis() / 1000).toInt(),
time = nowSeconds.toInt(),
precision_bits = 15,
)
.encode()
@ -225,14 +226,14 @@ constructor(
id = packetIdSequence.next(),
from = numIn,
to = 0xffffffff.toInt(), // broadcast
rx_time = (System.currentTimeMillis() / 1000).toInt(),
rx_time = nowSeconds.toInt(),
rx_snr = 1.5f,
decoded =
Data(
portnum = PortNum.TELEMETRY_APP,
payload =
Telemetry(
time = (System.currentTimeMillis() / 1000).toInt(),
time = nowSeconds.toInt(),
device_metrics =
DeviceMetrics(
battery_level = 85,
@ -254,7 +255,7 @@ constructor(
id = packetIdSequence.next(),
from = numIn,
to = 0xffffffff.toInt(), // broadcast
rx_time = (System.currentTimeMillis() / 1000).toInt(),
rx_time = nowSeconds.toInt(),
rx_snr = 1.5f,
decoded =
Data(
@ -271,7 +272,7 @@ constructor(
id = packetIdSequence.next(),
from = fromIn,
to = toIn,
rx_time = (System.currentTimeMillis() / 1000).toInt(),
rx_time = nowSeconds.toInt(),
rx_snr = 1.5f,
decoded = data,
),
@ -326,7 +327,7 @@ constructor(
latitude_i = org.meshtastic.core.model.Position.degI(lat),
longitude_i = org.meshtastic.core.model.Position.degI(lon),
altitude = 35,
time = (System.currentTimeMillis() / 1000).toInt(),
time = nowSeconds.toInt(),
precision_bits = Random.nextInt(10, 19),
),
),
@ -345,6 +346,7 @@ constructor(
makeNodeInfo(MY_NODE, 32.776665, -96.796989), // dallas
makeNodeInfo(MY_NODE + 1, 32.960758, -96.733521), // richardson
FromRadio(config = Config(lora = defaultLoRaConfig)),
FromRadio(config = Config(lora = defaultLoRaConfig)),
FromRadio(channel = defaultChannel),
FromRadio(config_complete_id = configId),

View file

@ -52,6 +52,7 @@ import no.nordicsemi.kotlin.ble.client.exception.InvalidAttributeException
import no.nordicsemi.kotlin.ble.core.CharacteristicProperty
import no.nordicsemi.kotlin.ble.core.ConnectionState
import no.nordicsemi.kotlin.ble.core.WriteType
import org.meshtastic.core.model.util.nowMillis
import java.util.UUID
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.toKotlinUuid
@ -170,19 +171,19 @@ constructor(
private fun connect() {
connectionScope.launch {
try {
connectionStartTime = System.currentTimeMillis()
connectionStartTime = nowMillis
Logger.i { "[$address] BLE connection attempt started at $connectionStartTime" }
peripheral = retryCall { findAndConnectPeripheral() }
peripheral?.let {
val connectionTime = System.currentTimeMillis() - connectionStartTime
val connectionTime = nowMillis - connectionStartTime
Logger.i { "[$address] BLE peripheral connected in ${connectionTime}ms" }
onConnected()
observePeripheralChanges()
discoverServicesAndSetupCharacteristics(it)
}
} catch (e: Exception) {
val failureTime = System.currentTimeMillis() - connectionStartTime
val failureTime = nowMillis - connectionStartTime
// BLE connection errors are common and often transient
Logger.w(e) { "[$address] Failed to connect to peripheral after ${failureTime}ms" }
service.onDisconnect(BleError.from(e))
@ -230,7 +231,7 @@ constructor(
val uptime =
if (connectionStartTime > 0) {
System.currentTimeMillis() - connectionStartTime
nowMillis - connectionStartTime
} else {
0
}
@ -417,7 +418,7 @@ constructor(
runBlocking {
val uptime =
if (connectionStartTime > 0) {
System.currentTimeMillis() - connectionStartTime
nowMillis - connectionStartTime
} else {
0
}

View file

@ -47,6 +47,7 @@ import org.meshtastic.core.analytics.platform.PlatformAnalytics
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.di.ProcessLifecycle
import org.meshtastic.core.model.util.anonymize
import org.meshtastic.core.model.util.nowMillis
import org.meshtastic.core.prefs.radio.RadioPrefs
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.proto.Heartbeat
@ -146,7 +147,7 @@ constructor(
private var lastHeartbeatMillis = 0L
fun keepAlive(now: Long = System.currentTimeMillis()) {
fun keepAlive(now: Long = nowMillis) {
if (now - lastHeartbeatMillis > HEARTBEAT_INTERVAL_MILLIS) {
if (radioIf is SerialInterface) {
Logger.i { "Sending ToRadio heartbeat" }

View file

@ -22,6 +22,7 @@ import com.geeksville.mesh.repository.usb.SerialConnectionListener
import com.geeksville.mesh.repository.usb.UsbRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import org.meshtastic.core.model.util.nowMillis
import java.util.concurrent.atomic.AtomicReference
/** An interface that assumes we are talking to a meshtastic device via USB serial */
@ -49,7 +50,7 @@ constructor(
if (device == null) {
Logger.e { "[$address] Serial device not found at address" }
} else {
val connectStart = System.currentTimeMillis()
val connectStart = nowMillis
Logger.i { "[$address] Opening serial device: $device" }
var packetsReceived = 0
@ -57,7 +58,7 @@ constructor(
var connectionStartTime = 0L
val onConnect: () -> Unit = {
connectionStartTime = System.currentTimeMillis()
connectionStartTime = nowMillis
val connectionTime = connectionStartTime - connectStart
Logger.i { "[$address] Serial device connected in ${connectionTime}ms" }
super.connect()
@ -90,7 +91,7 @@ constructor(
override fun onDisconnected(thrown: Exception?) {
val uptime =
if (connectionStartTime > 0) {
System.currentTimeMillis() - connectionStartTime
nowMillis - connectionStartTime
} else {
0
}

View file

@ -25,6 +25,7 @@ import dagger.assisted.AssistedInject
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.util.nowMillis
import org.meshtastic.proto.Heartbeat
import org.meshtastic.proto.ToRadio
import java.io.BufferedInputStream
@ -104,7 +105,7 @@ constructor(
if (s != null) {
val uptime =
if (connectionStartTime > 0) {
System.currentTimeMillis() - connectionStartTime
nowMillis - connectionStartTime
} else {
0
}
@ -130,7 +131,7 @@ constructor(
} catch (ex: IOException) {
val uptime =
if (connectionStartTime > 0) {
System.currentTimeMillis() - connectionStartTime
nowMillis - connectionStartTime
} else {
0
}
@ -140,7 +141,7 @@ constructor(
} catch (ex: Throwable) {
val uptime =
if (connectionStartTime > 0) {
System.currentTimeMillis() - connectionStartTime
nowMillis - connectionStartTime
} else {
0
}
@ -175,7 +176,7 @@ constructor(
// Create a socket to make the connection with the server
private suspend fun startConnect() = withContext(dispatchers.io) {
val attemptStart = System.currentTimeMillis()
val attemptStart = nowMillis
Logger.i { "[$address] TCP connection attempt starting..." }
val parts = address.split(":", limit = 2)
@ -190,8 +191,8 @@ constructor(
socket.soTimeout = SOCKET_TIMEOUT
this@TCPInterface.socket = socket
val connectTime = System.currentTimeMillis() - attemptStart
connectionStartTime = System.currentTimeMillis()
val connectTime = nowMillis - attemptStart
connectionStartTime = nowMillis
Logger.i {
"[$address] TCP socket connected in ${connectTime}ms - " +
"Local: ${socket.localSocketAddress}, Remote: ${socket.remoteSocketAddress}"

View file

@ -22,11 +22,12 @@ import com.geeksville.mesh.util.ignoreException
import com.hoho.android.usbserial.driver.UsbSerialDriver
import com.hoho.android.usbserial.driver.UsbSerialPort
import com.hoho.android.usbserial.util.SerialInputOutputManager
import org.meshtastic.core.model.util.await
import java.nio.BufferOverflowException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import kotlin.time.Duration.Companion.seconds
internal class SerialConnectionImpl(
private val usbManagerLazy: dagger.Lazy<UsbManager?>,
@ -62,7 +63,7 @@ internal class SerialConnectionImpl(
// Allow a short amount of time for the manager to quit (so the port can be cleanly closed)
if (waitForStopped) {
Logger.d { "Waiting for USB manager to stop..." }
closedLatch.await(1, TimeUnit.SECONDS)
closedLatch.await(1.seconds)
}
}
}

View file

@ -25,6 +25,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.model.util.nowMillis
import org.meshtastic.core.service.MeshServiceNotifications
import javax.inject.Inject
@ -48,7 +49,7 @@ class MarkAsReadReceiver : BroadcastReceiver() {
val pendingResult = goAsync()
scope.launch {
try {
packetRepository.clearUnreadCount(contactKey, System.currentTimeMillis())
packetRepository.clearUnreadCount(contactKey, nowMillis)
meshServiceNotifications.cancelMessageNotification(contactKey)
} finally {
pendingResult.finish()

View file

@ -31,6 +31,7 @@ import org.meshtastic.core.database.entity.ReactionEntity
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.Position
import org.meshtastic.core.model.util.nowMillis
import org.meshtastic.core.prefs.mesh.MeshPrefs
import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.core.service.ServiceAction
@ -160,7 +161,7 @@ constructor(
replyId = action.replyId,
userId = nodeManager.getMyId().takeIf { it.isNotEmpty() } ?: DataPacket.ID_LOCAL,
emoji = action.emoji,
timestamp = System.currentTimeMillis(),
timestamp = nowMillis,
snr = 0f,
rssi = 0,
hopsAway = 0,

View file

@ -33,6 +33,8 @@ import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.Position
import org.meshtastic.core.model.TelemetryType
import org.meshtastic.core.model.util.isWithinSizeLimit
import org.meshtastic.core.model.util.nowMillis
import org.meshtastic.core.model.util.nowSeconds
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.ChannelSet
@ -64,7 +66,7 @@ constructor(
private val radioConfigRepository: RadioConfigRepository?,
) {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val currentPacketId = AtomicLong(java.util.Random(System.currentTimeMillis()).nextLong().absoluteValue)
private val currentPacketId = AtomicLong(java.util.Random(nowMillis).nextLong().absoluteValue)
private val sessionPasskey = AtomicReference(ByteString.EMPTY)
private val offlineSentPackets = CopyOnWriteArrayList<DataPacket>()
val tracerouteStartTimes = ConcurrentHashMap<Int, Long>()
@ -175,7 +177,7 @@ constructor(
emoji = p.emoji,
),
)
p.time = System.currentTimeMillis()
p.time = nowMillis
packetHandler?.sendToRadio(meshPacket)
}
@ -240,7 +242,7 @@ constructor(
latitude_i = Position.degI(currentPosition.latitude),
longitude_i = Position.degI(currentPosition.longitude),
altitude = currentPosition.altitude,
time = (System.currentTimeMillis() / TIME_MS_TO_S).toInt(),
time = nowSeconds.toInt(),
)
packetHandler?.sendToRadio(
buildMeshPacket(
@ -292,7 +294,7 @@ constructor(
}
fun requestTraceroute(requestId: Int, destNum: Int) {
tracerouteStartTimes[requestId] = System.currentTimeMillis()
tracerouteStartTimes[requestId] = nowMillis
packetHandler?.sendToRadio(
buildMeshPacket(
to = destNum,
@ -343,7 +345,7 @@ constructor(
}
fun requestNeighborInfo(requestId: Int, destNum: Int) {
neighborInfoStartTimes[requestId] = System.currentTimeMillis()
neighborInfoStartTimes[requestId] = nowMillis
val myNum = nodeManager?.myNodeNum ?: 0
if (destNum == myNum) {
val neighborInfoToSend =
@ -360,7 +362,7 @@ constructor(
Neighbor(
node_id = 0, // Dummy node ID that can be intercepted
snr = 0f,
last_rx_time = (System.currentTimeMillis() / TIME_MS_TO_S).toInt(),
last_rx_time = nowSeconds.toInt(),
node_broadcast_interval_secs = oneHour,
),
),
@ -471,7 +473,6 @@ constructor(
companion object {
private const val PACKET_ID_MASK = 0xffffffffL
private const val PACKET_ID_SHIFT_BITS = 32
private const val TIME_MS_TO_S = 1000L
private const val ADMIN_CHANNEL_NAME = "admin"
private const val NODE_ID_PREFIX = "!"

View file

@ -34,6 +34,8 @@ import org.meshtastic.core.analytics.DataPair
import org.meshtastic.core.analytics.platform.PlatformAnalytics
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.model.util.nowMillis
import org.meshtastic.core.model.util.nowSeconds
import org.meshtastic.core.prefs.ui.UiPrefs
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.service.MeshServiceNotifications
@ -48,7 +50,9 @@ import org.meshtastic.proto.Telemetry
import org.meshtastic.proto.ToRadio
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
@Suppress("LongParameterList", "TooManyFunctions")
@Singleton
@ -140,7 +144,7 @@ constructor(
}
serviceBroadcasts.broadcastConnection()
Logger.d { "Starting connect" }
connectTimeMsec = System.currentTimeMillis()
connectTimeMsec = nowMillis
scope.handledLaunch { nodeRepository.clearMyNodeInfo() }
startConfigOnly()
}
@ -152,12 +156,12 @@ constructor(
mqttManager.stop()
if (connectTimeMsec != 0L) {
val now = System.currentTimeMillis()
val now = nowMillis
val duration = now - connectTimeMsec
connectTimeMsec = 0L
analytics.track(
EVENT_CONNECTED_SECONDS,
DataPair(EVENT_CONNECTED_SECONDS, duration / MILLISECONDS_IN_SECOND),
DataPair(EVENT_CONNECTED_SECONDS, duration.milliseconds.toDouble(DurationUnit.SECONDS)),
)
}
@ -207,9 +211,7 @@ constructor(
val myNodeNum = nodeManager.myNodeNum ?: 0
// Set time
commandSender.sendAdmin(myNodeNum) {
AdminMessage(set_time_only = (System.currentTimeMillis() / MILLISECONDS_IN_SECOND).toInt())
}
commandSender.sendAdmin(myNodeNum) { AdminMessage(set_time_only = nowSeconds.toInt()) }
}
fun onNodeDbReady() {
@ -268,7 +270,6 @@ constructor(
companion object {
private const val CONFIG_ONLY_NONCE = 69420
private const val NODE_INFO_NONCE = 69421
private const val MILLISECONDS_IN_SECOND = 1000.0
private const val DEVICE_SLEEP_TIMEOUT_SECONDS = 30
private const val EVENT_CONNECTED_SECONDS = "connected_seconds"

View file

@ -39,6 +39,8 @@ import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.util.SfppHasher
import org.meshtastic.core.model.util.decodeOrNull
import org.meshtastic.core.model.util.nowMillis
import org.meshtastic.core.model.util.nowSeconds
import org.meshtastic.core.model.util.toOneLiner
import org.meshtastic.core.prefs.mesh.MeshPrefs
import org.meshtastic.core.service.MeshServiceNotifications
@ -325,7 +327,7 @@ constructor(
val payload = packet.decoded?.payload ?: return
val u = Waypoint.ADAPTER.decode(payload)
if (u.locked_to != 0 && u.locked_to != packet.from) return
val currentSecond = (System.currentTimeMillis() / MILLISECONDS_IN_SECOND).toInt()
val currentSecond = nowSeconds.toInt()
rememberDataPacket(dataPacket, myNodeNum, updateNotification = u.expire > currentSecond)
}
@ -443,7 +445,7 @@ constructor(
isRemote -> shouldDisplay = true
}
if (shouldDisplay) {
val now = System.currentTimeMillis() / MILLISECONDS_IN_SECOND
val now = nowSeconds
if (!batteryPercentCooldowns.containsKey(fromNum)) batteryPercentCooldowns[fromNum] = 0L
if ((now - batteryPercentCooldowns[fromNum]!!) >= BATTERY_PERCENT_COOLDOWN_SECONDS || forceDisplay) {
batteryPercentCooldowns[fromNum] = now
@ -461,7 +463,7 @@ constructor(
}
handleAckNak(
packet.decoded?.request_id ?: 0,
dataMapper.toNodeID(packet.from),
nodeManager.toNodeID(packet.from),
r.error_reason?.value ?: 0,
dataPacket.relayNode,
)
@ -599,7 +601,7 @@ constructor(
packetId = dataPacket.id,
port_num = dataPacket.dataType,
contact_key = contactKey,
received_time = System.currentTimeMillis(),
received_time = nowMillis,
read = fromLocal || isFiltered,
data = dataPacket,
snr = dataPacket.snr,
@ -693,8 +695,8 @@ constructor(
private fun rememberReaction(packet: MeshPacket) = scope.handledLaunch {
val decoded = packet.decoded ?: return@handledLaunch
val emoji = decoded.payload.toByteArray().decodeToString()
val fromId = dataMapper.toNodeID(packet.from)
val toId = dataMapper.toNodeID(packet.to)
val fromId = nodeManager.toNodeID(packet.from)
val toId = nodeManager.toNodeID(packet.to)
val reaction =
ReactionEntity(
@ -702,7 +704,7 @@ constructor(
replyId = decoded.reply_id,
userId = fromId,
emoji = emoji,
timestamp = System.currentTimeMillis(),
timestamp = nowMillis,
snr = packet.rx_snr,
rssi = packet.rx_rssi,
hopsAway =
@ -790,7 +792,6 @@ constructor(
}
companion object {
private const val MILLISECONDS_IN_SECOND = 1000L
private const val HOPS_AWAY_UNAVAILABLE = -1
private const val BATTERY_PERCENT_UNSUPPORTED = 0.0

View file

@ -30,6 +30,8 @@ import kotlinx.coroutines.flow.onEach
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.model.util.isLora
import org.meshtastic.core.model.util.nowMillis
import org.meshtastic.core.model.util.nowSeconds
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.proto.FromRadio
import org.meshtastic.proto.LogRecord
@ -41,7 +43,6 @@ import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration.Companion.milliseconds
@Suppress("TooManyFunctions")
@Singleton
@ -126,7 +127,7 @@ constructor(
MeshLog(
uuid = UUID.randomUUID().toString(),
message_type = type,
received_date = System.currentTimeMillis(),
received_date = nowMillis,
raw_message = message,
fromRadio = proto,
),
@ -136,7 +137,7 @@ constructor(
fun handleReceivedMeshPacket(packet: MeshPacket, myNodeNum: Int?) {
val rxTime =
if (packet.rx_time == 0) {
(System.currentTimeMillis().milliseconds.inWholeSeconds).toInt()
nowSeconds.toInt()
} else {
packet.rx_time
}
@ -186,7 +187,7 @@ constructor(
MeshLog(
uuid = UUID.randomUUID().toString(),
message_type = "Packet",
received_date = System.currentTimeMillis(),
received_date = nowMillis,
raw_message = packet.toString(),
fromNum = packet.from,
portNum = decoded.portnum.value,
@ -201,9 +202,7 @@ constructor(
myNodeNum?.let { myNum ->
val from = packet.from
val isOtherNode = myNum != from
nodeManager.updateNodeInfo(myNum, withBroadcast = isOtherNode) {
it.lastHeard = (System.currentTimeMillis().milliseconds.inWholeSeconds).toInt()
}
nodeManager.updateNodeInfo(myNum, withBroadcast = isOtherNode) { it.lastHeard = nowSeconds.toInt() }
nodeManager.updateNodeInfo(from, withBroadcast = false, channel = packet.channel) {
it.lastHeard = packet.rx_time
it.viaMqtt = packet.via_mqtt == true

View file

@ -21,6 +21,7 @@ import com.meshtastic.core.strings.getString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import org.meshtastic.core.model.util.nowMillis
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.unknown_username
@ -74,7 +75,7 @@ constructor(
val responseText =
if (start != null) {
val elapsedMs = System.currentTimeMillis() - start
val elapsedMs = nowMillis - start
val seconds = elapsedMs / MILLIS_PER_SECOND
Logger.i { "Neighbor info $requestId complete in $seconds s" }
String.format(Locale.US, "%s\n\nDuration: %.1f s", formatted, seconds)

View file

@ -32,6 +32,7 @@ import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MyNodeInfo
import org.meshtastic.core.model.NodeInfo
import org.meshtastic.core.model.Position
import org.meshtastic.core.model.util.nowMillis
import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.proto.DeviceMetadata
import org.meshtastic.proto.HardwareModel
@ -186,12 +187,7 @@ constructor(
}
}
fun handleReceivedPosition(
fromNum: Int,
myNodeNum: Int,
p: ProtoPosition,
defaultTime: Long = System.currentTimeMillis(),
) {
fun handleReceivedPosition(fromNum: Int, myNodeNum: Int, p: ProtoPosition, defaultTime: Long = nowMillis) {
if (myNodeNum == fromNum && (p.latitude_i ?: 0) == 0 && (p.longitude_i ?: 0) == 0) {
Logger.d { "Ignoring nop position update for the local node" }
} else {

View file

@ -52,6 +52,7 @@ import org.meshtastic.core.database.entity.NodeEntity
import org.meshtastic.core.database.model.Message
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.util.formatUptime
import org.meshtastic.core.model.util.nowMillis
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.core.service.SERVICE_NOTIFY_ID
@ -78,6 +79,8 @@ import org.meshtastic.proto.DeviceMetrics
import org.meshtastic.proto.LocalStats
import org.meshtastic.proto.Telemetry
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration.Companion.minutes
/**
* Manages the creation and display of all app notifications.
@ -86,6 +89,7 @@ import javax.inject.Inject
* notifications for various events like new messages, alerts, and service status changes.
*/
@Suppress("TooManyFunctions", "LongParameterList")
@Singleton
class MeshServiceNotificationsImpl
@Inject
constructor(
@ -97,7 +101,6 @@ constructor(
private val notificationManager = context.getSystemService<NotificationManager>()!!
companion object {
private const val FIFTEEN_MINUTES_IN_MILLIS = 15L * 60 * 1000
const val MAX_BATTERY_LEVEL = 100
private val NOTIFICATION_LIGHT_COLOR = Color.BLUE
private const val MAX_HISTORY_MESSAGES = 10
@ -107,6 +110,8 @@ constructor(
private const val SUMMARY_ID = 1
private const val PERSON_ICON_SIZE = 128
private const val PERSON_ICON_TEXT_SIZE_RATIO = 0.5f
private const val STATS_UPDATE_MINUTES = 15
private val STATS_UPDATE_INTERVAL = STATS_UPDATE_MINUTES.minutes
}
/**
@ -279,7 +284,7 @@ constructor(
hasLocalStats -> {
val localStatsMessage = telemetry?.local_stats?.formatToString()
cachedTelemetry = telemetry
nextStatsUpdateMillis = System.currentTimeMillis() + FIFTEEN_MINUTES_IN_MILLIS
nextStatsUpdateMillis = nowMillis + STATS_UPDATE_INTERVAL.inWholeMilliseconds
localStatsMessage
}
cachedTelemetry == null && hasDeviceMetrics -> {
@ -287,7 +292,7 @@ constructor(
if (cachedLocalStats == null) {
cachedTelemetry = telemetry
}
nextStatsUpdateMillis = System.currentTimeMillis()
nextStatsUpdateMillis = nowMillis
deviceMetricsMessage
}
else -> null
@ -471,7 +476,7 @@ constructor(
}
nextUpdateAt
?.takeIf { it > System.currentTimeMillis() }
?.takeIf { it > nowMillis }
?.let {
builder.setWhen(it)
builder.setUsesChronometer(true)
@ -579,7 +584,7 @@ constructor(
isSilent: Boolean,
): Notification {
val person = Person.Builder().setName(name).build()
val style = NotificationCompat.MessagingStyle(person).addMessage(message, System.currentTimeMillis(), person)
val style = NotificationCompat.MessagingStyle(person).addMessage(message, nowMillis, person)
val builder =
commonBuilder(NotificationType.Waypoint, createOpenWaypointIntent(waypointId))
@ -588,7 +593,7 @@ constructor(
.setStyle(style)
.setGroup(GROUP_KEY_MESSAGES)
.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
.setWhen(System.currentTimeMillis())
.setWhen(nowMillis)
.setShowWhen(true)
if (isSilent) {
@ -600,7 +605,7 @@ constructor(
private fun createAlertNotification(contactKey: String, name: String, alert: String): Notification {
val person = Person.Builder().setName(name).build()
val style = NotificationCompat.MessagingStyle(person).addMessage(alert, System.currentTimeMillis(), person)
val style = NotificationCompat.MessagingStyle(person).addMessage(alert, nowMillis, person)
return commonBuilder(NotificationType.Alert, createOpenMessageIntent(contactKey))
.setPriority(NotificationCompat.PRIORITY_HIGH)
@ -617,7 +622,7 @@ constructor(
.setCategory(Notification.CATEGORY_STATUS)
.setAutoCancel(true)
.setContentTitle(title)
.setWhen(System.currentTimeMillis())
.setWhen(nowMillis)
.setShowWhen(true)
.setContentText(message)
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
@ -639,7 +644,7 @@ constructor(
.setContentTitle(title)
.setContentText(message)
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
.setWhen(System.currentTimeMillis())
.setWhen(nowMillis)
.setShowWhen(true)
.build()
}
@ -738,7 +743,7 @@ constructor(
Intent(context, ReactionReceiver::class.java).apply {
action = REACT_ACTION
putExtra(ReactionReceiver.EXTRA_CONTACT_KEY, contactKey)
putExtra(ReactionReceiver.EXTRA_PACKET_ID, packetId)
putExtra(ReactionReceiver.EXTRA_REPLY_ID, packetId)
putExtra(ReactionReceiver.EXTRA_TO_ID, toId)
putExtra(ReactionReceiver.EXTRA_CHANNEL_INDEX, channelIndex)
putExtra(ReactionReceiver.EXTRA_EMOJI, "👍")

View file

@ -26,6 +26,7 @@ import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.TracerouteSnapshotRepository
import org.meshtastic.core.model.fullRouteDiscovery
import org.meshtastic.core.model.getFullTracerouteResponse
import org.meshtastic.core.model.util.nowMillis
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.service.TracerouteResponse
import org.meshtastic.core.strings.Res
@ -83,8 +84,8 @@ constructor(
val start = commandSender.tracerouteStartTimes.remove(requestId)
val responseText =
if (start != null) {
val elapsedMs = System.currentTimeMillis() - start
val seconds = elapsedMs / MILLISECONDS_IN_SECOND
val elapsedMs = nowMillis - start
val seconds = elapsedMs / MILLIS_PER_SECOND
Logger.i { "Traceroute $requestId complete in $seconds s" }
val durationText = getString(Res.string.traceroute_duration, "%.1f".format(Locale.US, seconds))
"$full\n\n$durationText"
@ -108,6 +109,6 @@ constructor(
}
companion object {
private const val MILLISECONDS_IN_SECOND = 1000.0
private const val MILLIS_PER_SECOND = 1000.0
}
}

View file

@ -33,6 +33,7 @@ import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.util.nowMillis
import org.meshtastic.core.model.util.toOneLineString
import org.meshtastic.core.model.util.toPIIString
import org.meshtastic.core.service.ConnectionState
@ -45,6 +46,8 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
@Suppress("TooManyFunctions")
@Singleton
@ -59,7 +62,7 @@ constructor(
) {
companion object {
private const val TIMEOUT_MS = 5000L // Increased from 250ms to be more tolerant
private val TIMEOUT = 5.seconds // Increased from 250ms to be more tolerant
}
private var queueJob: Job? = null
@ -89,7 +92,7 @@ constructor(
MeshLog(
uuid = UUID.randomUUID().toString(),
message_type = "Packet",
received_date = System.currentTimeMillis(),
received_date = nowMillis,
raw_message = packet.toString(),
fromNum = packet.from ?: 0,
portNum = packet.decoded?.portnum?.value ?: 0,
@ -148,7 +151,7 @@ constructor(
// send packet to the radio and wait for response
val response = sendPacket(packet)
Logger.d { "queueJob packet id=${packet.id.toUInt()} waiting" }
val success = withTimeout(TIMEOUT_MS) { response.await() }
val success = withTimeout(TIMEOUT) { response.await() }
Logger.d { "queueJob packet id=${packet.id.toUInt()} success $success" }
} catch (e: TimeoutCancellationException) {
Logger.d { "queueJob packet id=${packet.id.toUInt()} timeout" }
@ -173,11 +176,11 @@ constructor(
}
@Suppress("MagicNumber")
private suspend fun getDataPacketById(packetId: Int): DataPacket? = withTimeoutOrNull(1000) {
private suspend fun getDataPacketById(packetId: Int): DataPacket? = withTimeoutOrNull(1.seconds) {
var dataPacket: DataPacket? = null
while (dataPacket == null) {
dataPacket = packetRepository.get().getPacketById(packetId)?.data
if (dataPacket == null) delay(100)
if (dataPacket == null) delay(100.milliseconds)
}
dataPacket
}

View file

@ -19,91 +19,48 @@ package com.geeksville.mesh.service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import co.touchlab.kermit.Logger
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import okio.ByteString.Companion.toByteString
import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.database.entity.ReactionEntity
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.proto.PortNum
import org.meshtastic.core.service.ServiceAction
import org.meshtastic.core.service.ServiceRepository
import javax.inject.Inject
@AndroidEntryPoint
class ReactionReceiver : BroadcastReceiver() {
@Inject lateinit var commandSender: MeshCommandSender
@Inject lateinit var meshServiceNotifications: MeshServiceNotifications
@Inject lateinit var serviceRepository: ServiceRepository
@Inject lateinit var packetRepository: PacketRepository
@Inject lateinit var nodeManager: MeshNodeManager
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
companion object {
const val REACT_ACTION = "com.geeksville.mesh.REACT_ACTION"
const val EXTRA_PACKET_ID = "packetId"
const val EXTRA_EMOJI = "emoji"
const val EXTRA_CONTACT_KEY = "contactKey"
const val EXTRA_TO_ID = "toId"
const val EXTRA_CHANNEL_INDEX = "channelIndex"
private const val EMOJI_INDICATOR = 1
}
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@Suppress("TooGenericExceptionCaught", "ReturnCount")
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != REACT_ACTION) return
val pendingResult = goAsync()
val contactKey = intent.getStringExtra(EXTRA_CONTACT_KEY) ?: return
val reaction = intent.getStringExtra(EXTRA_EMOJI) ?: intent.getStringExtra(EXTRA_REACTION) ?: return
val replyId = intent.getIntExtra(EXTRA_REPLY_ID, intent.getIntExtra(EXTRA_PACKET_ID, 0))
scope.launch {
try {
val packetId = intent.getIntExtra(EXTRA_PACKET_ID, 0)
val emoji = intent.getStringExtra(EXTRA_EMOJI)
val toId = intent.getStringExtra(EXTRA_TO_ID)
val channelIndex = intent.getIntExtra(EXTRA_CHANNEL_INDEX, 0)
val contactKey = intent.getStringExtra(EXTRA_CONTACT_KEY)
@Suppress("ComplexCondition")
if (packetId == 0 || emoji.isNullOrEmpty() || toId.isNullOrEmpty() || contactKey.isNullOrEmpty()) {
return@launch
}
// Reactions are text messages with a replyId and emoji set
val reactionPacket =
DataPacket(
to = toId,
channel = channelIndex,
bytes = emoji.encodeToByteArray().toByteString(),
dataType = PortNum.TEXT_MESSAGE_APP.value,
replyId = packetId,
wantAck = true,
emoji = EMOJI_INDICATOR,
)
commandSender.sendData(reactionPacket)
val reaction =
ReactionEntity(
myNodeNum = nodeManager.myNodeNum ?: 0,
replyId = packetId,
userId = nodeManager.getMyId().takeIf { it.isNotEmpty() } ?: DataPacket.ID_LOCAL,
emoji = emoji,
timestamp = System.currentTimeMillis(),
packetId = reactionPacket.id,
status = MessageStatus.QUEUED,
to = toId,
channel = channelIndex,
)
packetRepository.insertReaction(reaction)
// Dismiss the notification after reacting
meshServiceNotifications.cancelMessageNotification(contactKey)
} finally {
pendingResult.finish()
serviceRepository.onServiceAction(ServiceAction.Reaction(reaction, replyId, contactKey))
} catch (e: Exception) {
Logger.e(e) { "Error sending reaction" }
}
}
}
companion object {
const val REACT_ACTION = "com.geeksville.mesh.REACT_ACTION"
const val EXTRA_CONTACT_KEY = "extra_contact_key"
const val EXTRA_REACTION = "extra_reaction"
const val EXTRA_REPLY_ID = "extra_reply_id"
const val EXTRA_PACKET_ID = "extra_packet_id"
const val EXTRA_TO_ID = "extra_to_id"
const val EXTRA_CHANNEL_INDEX = "extra_channel_index"
const val EXTRA_EMOJI = "extra_emoji"
}
}

View file

@ -40,6 +40,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -66,8 +67,10 @@ import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.pluralStringResource
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.entity.ContactSettings
import org.meshtastic.core.model.util.TimeConstants
import org.meshtastic.core.model.util.formatMuteRemainingTime
import org.meshtastic.core.model.util.getChannel
import org.meshtastic.core.model.util.nowMillis
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.are_you_sure
import org.meshtastic.core.strings.cancel
@ -104,7 +107,7 @@ import org.meshtastic.core.ui.icon.VolumeUpTwoTone
import org.meshtastic.core.ui.qr.ScannedQrCodeDialog
import org.meshtastic.core.ui.util.showToast
import org.meshtastic.proto.ChannelSet
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.days
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalPermissionsApi::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@ -172,7 +175,7 @@ fun ContactsScreen(
.filter { it.contactKey in selectedContactKeys }
}
// Get message count directly from repository for selected contacts
var selectedCount by remember { mutableStateOf(0) }
var selectedCount by remember { mutableIntStateOf(0) }
LaunchedEffect(selectedContactKeys.size, selectedContactKeys.joinToString(",")) {
selectedCount = viewModel.getTotalMessageCount(selectedContactKeys.toList())
}
@ -323,14 +326,14 @@ private fun MuteNotificationsDialog(
val muteOptions = remember {
listOf(
Res.string.unmute to 0L,
Res.string.mute_8_hours to TimeUnit.HOURS.toMillis(8),
Res.string.mute_1_week to TimeUnit.DAYS.toMillis(7),
Res.string.mute_8_hours to TimeConstants.EIGHT_HOURS.inWholeMilliseconds,
Res.string.mute_1_week to 7.days.inWholeMilliseconds,
Res.string.mute_always to Long.MAX_VALUE,
)
}
// State to hold the selected mute duration index
var selectedOptionIndex by remember { mutableStateOf(2) } // Default to "Always"
var selectedOptionIndex by remember { mutableIntStateOf(2) } // Default to "Always"
MeshtasticDialog(
onDismiss = onDismiss, // Dismiss the dialog when clicked outside
@ -347,7 +350,7 @@ private fun MuteNotificationsDialog(
// Show current mute status
selectedContactKeys.forEach { contactKey ->
contactSettings[contactKey]?.let { settings ->
val now = System.currentTimeMillis()
val now = nowMillis
val statusText =
when {
settings.muteUntil > 0 && settings.muteUntil != Long.MAX_VALUE -> {