From c4c056e41b7856af483e95c7baf327caff0139a4 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Fri, 13 Nov 2026 21:18:02 +0000 Subject: [PATCH] Handle GNSS fix times sent in Unix seconds Some vehicles send dynamics.vehicle.gnss's time in Unix seconds. Those fell below the 2020 cutoff and were read as GPS-epoch milliseconds, giving fixes dated January 1980. Times below 10^11 are now taken as seconds before the GPS time check. Co-Authored-By: Claude Opus 5.5 --- src/rivian/parallax/dynamics.py | 10 +++++++--- src/rivian/parallax/proto/dynamics.proto | 2 +- tests/parallax/test_dynamics.py | 9 +++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/rivian/parallax/dynamics.py b/src/rivian/parallax/dynamics.py index 0420d32..5f374f4 100644 --- a/src/rivian/parallax/dynamics.py +++ b/src/rivian/parallax/dynamics.py @@ -93,16 +93,20 @@ def decode_gear(m: dynamics_pb2.Gear) -> dict[str, Any]: return {"gearStatus": _enum(_GEAR_MAP, m.gear, what="gear")} -# Some vehicles send the fix time as GPS time (milliseconds since the GPS -# epoch, 1980-01-06, without leap seconds) instead of Unix time. Any time -# before 2020 is taken as GPS time. +# Vehicles send the fix time in Unix milliseconds, Unix seconds, or GPS time +# (milliseconds since the GPS epoch, 1980-01-06, without leap seconds). Any +# time below 10^11 (1973 in ms, year 5138 in seconds) is taken as seconds, and +# then any time before 2020 as GPS time. _GPS_EPOCH_OFFSET_MS: Final = 315_964_800_000 _GPS_LEAP_SECONDS_MS: Final = 18_000 _GPS_TIME_CUTOFF_MS: Final = 1_577_836_800_000 # 2020-01-01 in Unix ms +_SECONDS_CUTOFF: Final = 100_000_000_000 def _gnss_unix_ms(time: int) -> int: """A GNSS fix time as Unix epoch milliseconds.""" + if 0 < time < _SECONDS_CUTOFF: + time *= 1000 if 0 < time < _GPS_TIME_CUTOFF_MS: return time + _GPS_EPOCH_OFFSET_MS - _GPS_LEAP_SECONDS_MS return time diff --git a/src/rivian/parallax/proto/dynamics.proto b/src/rivian/parallax/proto/dynamics.proto index 707d5e4..1a9731c 100644 --- a/src/rivian/parallax/proto/dynamics.proto +++ b/src/rivian/parallax/proto/dynamics.proto @@ -55,7 +55,7 @@ message Gnss { float position_vertical_error = 7; // meters float speed_error = 8; // m/s float bearing_error = 9; // degrees - int64 time = 10; // epoch ms (Unix, or GPS on some vehicles); the whole fix + int64 time = 10; // Unix ms, Unix seconds or GPS ms, by vehicle; the whole fix } // dynamics.vehicle.location diff --git a/tests/parallax/test_dynamics.py b/tests/parallax/test_dynamics.py index a88e7fa..e49034a 100644 --- a/tests/parallax/test_dynamics.py +++ b/tests/parallax/test_dynamics.py @@ -52,6 +52,15 @@ def test_gnss_gps_time() -> None: assert result["gnssTimeStamp"] == epoch(1790553428211) +def test_gnss_unix_seconds() -> None: + """A fix time in Unix seconds is converted to milliseconds.""" + result = decode( + "dynamics.vehicle.gnss", dynamics.Gnss(latitude=1.0, time=1794604023) + ) + assert result["gnssTimeStamp"] == epoch(1794604023000) + assert result["gnssLocation"]["timeStamp"] == epoch(1794604023000) + + def test_gnss_empty() -> None: """An empty payload has no fix, so nothing is reported.""" assert decode("dynamics.vehicle.gnss") == {}