64 lines
2.9 KiB
Kotlin
64 lines
2.9 KiB
Kotlin
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")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|