Files
MicrOBU/app/src/main/java/com/hawhamburg/micr0bu/data/GnssTimeSource.kt
T

64 lines
2.9 KiB
Kotlin
Raw Normal View History

package com.hawhamburg.micr0bu.data
import android.os.SystemClock
import android.util.Log
import com.hawhamburg.micr0bu.domain.asn1.ItsTime
import java.time.DateTimeException
/**
* Puts the timestamps this phone transmits on GNSS time instead of its own wall clock.
*
* ## Why
* Every CAM carries a generationDeltaTime and every GeoNetworking header a TST, and receivers use
* them to judge how fresh a message is and in what order messages came. Both used to come straight
* from `System.currentTimeMillis()`, so they were only as good as the phone's clock setting. On
* 2026-09-10 the bench phone was 24 minutes fast: automatic time had no source (no SIM, and the
* lab Wi-Fi has no internet time), so it had not set the clock once in 69 hours, and every CAM
* went out stamped 24 minutes in the future. A bike-mounted phone on the road is in exactly that
* position. GNSS time depends on none of it.
*
* ## How
* [SystemClock.currentGnssTimeClock] (API 29, this app's minSdk) is a UTC clock the platform keeps
* synchronised from GNSS fixes. One reading of it taken alongside the wall clock gives the wall
* clock's error, which is then applied to the fix's own timestamp. When GNSS time is unavailable,
* typically indoors before any satellite fix since boot, the wall clock is used unchanged.
*
* Which clock is in use is logged whenever it changes, with the measured error, so a capture shows
* where a given run's timestamps came from.
*
* Only the transmit path uses this. Everything else in the app stays on the wall clock, because
* received messages, sensor samples and trip records are all stamped with it and must stay
* comparable with one another.
*/
object GnssTimeSource {
private const val TAG = "GnssTimeSource"
/** Whether the last correction used GNSS time; null before the first. For change-only logging. */
@Volatile private var lastUsedGnss: Boolean? = null
/** [systemMs], a wall-clock reading, moved onto GNSS time where GNSS time is available. */
fun correct(systemMs: Long): Long {
val systemNow = System.currentTimeMillis()
val gnssNow = try {
SystemClock.currentGnssTimeClock().millis()
} catch (e: DateTimeException) {
null
}
noteSource(gnssNow, systemNow)
return ItsTime.onGnssTime(systemMs, gnssNow, systemNow)
}
private fun noteSource(gnssNow: Long?, systemNow: Long) {
val usingGnss = gnssNow != null
if (lastUsedGnss == usingGnss) return
lastUsedGnss = usingGnss
if (gnssNow != null) {
Log.i(TAG, "transmit timestamps now on GNSS time; phone clock is " +
"${"%+.1f".format((systemNow - gnssNow) / 1000.0)} s off")
} else {
Log.w(TAG, "GNSS time unavailable, transmit timestamps fall back to the phone clock, " +
"which has no automatic time source without a SIM or internet")
}
}
}