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.
42 lines
1.5 KiB
Kotlin
42 lines
1.5 KiB
Kotlin
package com.hawhamburg.micr0bu
|
|
|
|
import com.hawhamburg.micr0bu.domain.asn1.ItsTime
|
|
import org.junit.Assert.assertEquals
|
|
import org.junit.Test
|
|
|
|
/**
|
|
* Pins the arithmetic that moves a transmit timestamp from the phone's wall clock onto GNSS time.
|
|
*
|
|
* The cases come from the 2026-09-10 bench session. The sending phone's clock was 1456 s fast
|
|
* because it had no automatic time source, and every CAM it sent was stamped 24 minutes in the
|
|
* future. After a manual correction it was 6 s slow. Both have to come out on GNSS time.
|
|
*/
|
|
class ItsTimeTest {
|
|
|
|
private val gnssNow = 1_789_038_922_000L
|
|
|
|
@Test
|
|
fun `without a GNSS reading the wall-clock time is used unchanged`() {
|
|
assertEquals(1_000L, ItsTime.onGnssTime(systemMs = 1_000L, gnssNowMs = null, systemNowMs = 5_000L))
|
|
}
|
|
|
|
@Test
|
|
fun `a phone clock running fast is pulled back onto GNSS time`() {
|
|
val systemNow = gnssNow + 1_456_000L
|
|
// A fix the wall clock stamped 0.8 s ago. It must still be 0.8 s old afterwards.
|
|
val fix = systemNow - 800L
|
|
assertEquals(gnssNow - 800L, ItsTime.onGnssTime(fix, gnssNow, systemNow))
|
|
}
|
|
|
|
@Test
|
|
fun `a phone clock running slow is pushed forward onto GNSS time`() {
|
|
val systemNow = gnssNow - 6_000L
|
|
assertEquals(gnssNow - 250L, ItsTime.onGnssTime(systemNow - 250L, gnssNow, systemNow))
|
|
}
|
|
|
|
@Test
|
|
fun `an accurate phone clock is left where it is`() {
|
|
assertEquals(gnssNow - 40L, ItsTime.onGnssTime(gnssNow - 40L, gnssNow, gnssNow))
|
|
}
|
|
}
|