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") } } }