Send each CAM with its position vector, a rotating pseudonym and GNSS time
The app side of the firmware's CAM_TX_PV message. Until now the phone handed the ESP32 bare CAM bytes, so the GeoNetworking header around them could only carry the firmware's bench placeholders. GnPositionVector.fromCam builds the Source Position Vector from the same Cam the UPER is encoded from, so the two layers cannot disagree about where the rider is. Position is rounded exactly as CamUperCodec rounds it, heading wraps into 0..3599, and non-finite values become 0. PAI is set when Android's horizontal accuracy is at most 24.7 m, the 40 m itsGnPaiInterval/2 threshold converted from a 95% to a 68% confidence radius. UsbSerialTransport.sendCamTx sends 0x05 once the heartbeat advertises the capability and 0x01 otherwise, so this build still transmits against older firmware, and logs which path it is on. Pseudonyms. The station ID used to be created once per install and never changed, under a MAC that never changed either, so every CAM this phone ever sent was linkable to every other. PseudonymManager now owns the station ID and the MAC as one identity and replaces both together every 10 minutes, or immediately if the clock goes backwards. Both are persisted in a single edit, so a crash cannot leave them mismatched. MACs are locally administered unicast and can never equal the bench ping's. CamTransmitLoop takes the current pseudonym per CAM, and the two most recently retired IDs still count as ours, so a frame sent just before a rotation is not taken for a stranger. GNSS time. On 2026-09-10 the bench phone's clock was 24 minutes fast: with no SIM and no internet time it had no automatic time source, and every CAM went out stamped in the future. GnssTimeSource moves transmit timestamps onto SystemClock.currentGnssTimeClock() and falls back to the wall clock without a fix, logging which one is in use and the measured error. ItsTime is now the single rule for both the CAM's generationDeltaTime and the GN TST. Receive paths stay on the wall clock so everything they stamp remains comparable. The bench pinger keeps its fixed station 999999 and a fixed MAC, so a ping stays recognisable in a capture. 999999 now counts as ours only while this phone's pinger runs and for 5 s after it stops. The previous rule treated it as ours unconditionally, which hid another phone's pings on the same bench. Leap seconds are an open question, recorded in ItsTime: TimestampIts may be TAI-based, which would put it 5 s higher. 85 tests, 0 failures.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user