Skip to content

Judge track fixes by their own times - #95

Merged
4 commits merged into
mainfrom
track-fix-precedence
Nov 8, 2026
Merged

4 commits merged into
mainfrom
track-fix-precedence

Conversation

@blike

@blike blike commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

Follow-up to #93. The location writer now judges navigation and GNSS fixes by their own times instead of the server clock, and legacy counts as a dense track of its own by a simpler rule that lets go sooner. This fixes three edge cases from the #93 review: late navigation fixes being dropped, a gap of up to two minutes when navigation stops, and navigation fixes being held back after legacy goes quiet.

@MrSerious0, could you review this and, if you can, run it on your R2? I only have an R1S to test against.

Changes

  • location-writer.ts
    • Every navigation fix is stored, at its own time, however late it arrives. driveAt files a late one into the drive it was taken on, as it already did for late GNSS fixes.
    • A GNSS fix is stored unless a navigation fix is within 15 s of it (NAV_COVERS_MS). The 15 s is measured between the fixes' own times, not against the server clock. This replaces NAV_FIX_FRESH_MS.
    • A late fix isn't where the vehicle is now, so it no longer becomes the point later moves are measured from.
    • Duplicates are still caught by the (vehicle_id, ts) key. A frame seen again after a restart is stored once.
  • vehicle-monitor.ts
    • New density rule: legacy outranks the navigation fix while it has 3 distinct fixes within 30 s of it. This replaces the two-gaps check (LEGACY_DENSE_MS) and the five-minute limit (LEGACY_QUIET_MS).
    • preferLegacy no longer deletes the held navigation fix. It only stops accepting new ones. The writer stores a GNSS fix wherever no navigation fix is close to it, so a held fix can't keep legacy's out.
  • Tests
    • Updated: the tests that encoded the old rules.
    • New:
      • the 15 s handover, to the millisecond;
      • a late navigation fix filed under its drive;
      • a navigation fix seen again after a restart, stored once;
      • the 30 s density limit, to the millisecond;
      • a wake-up burst;
      • a dense feed going quiet;
      • a short move whose speed arrives in a later delta.

Why these numbers

From an R1S database (Oct 1 – Nov 4: 5,538 trip_progress frames, 279 drives, 33 with navigation running):

  • 15 s: the navigation stream ran at 5.0 s (p99 5.1 s). There were 12 gaps of 10–15 s and 5 of 15–60 s. So 15 s keeps the two feeds from interleaving, and GNSS still fills in as soon as navigation stops. A 10 s window would have interleaved them in those 12 places.
  • 3 fixes in 30 s: replayed second by second over the 33 navigated drives.
Rule for "legacy is dense" Switches Seconds on navigation fixes Seconds navigation was held back after legacy had been quiet over 30 s
#93: last two gaps ≤ 30 s, then up to 5 min 192 1,291 2,445
4 fixes in the last 60 s 132 2,534 925
3 fixes in the last 30 s (this PR) 254 4,377 0

An R1 reporting every 5 s qualifies within 10 s and lets go 20–30 s into a quiet spell. A wake-up burst on an R2 holds for 30 s at most.

The extra switches happen mostly at stops, where neither feed stores a point. The replay treats stops as quiet spells, because the stored fixes skip a car that isn't moving.

Testing

  • Unit tests added/updated
  • Integration tests passed on a fresh database: server 621 tests (69 files), with the database tests
  • Existing tests passed: web 288; typecheck, lint and Prettier clean
  • Run live on an R2

Deployment

  • No special deployment requirements

Additional Notes

  • One zigzag point possible as navigation starts. A GNSS fix stored just before the first navigation fix can be within 15 s of it, so the start of a navigation stretch can have one zigzag point.
  • Late frames haven't been seen yet. I couldn't measure whether Parallax ever delivers trip_progress frames late. The archive keeps Rivian's time, not arrival time, and 6.5 matches the frame's time (p99 0.1 s, max 2.2 s). If frames are never late, the late-fix handling is just a safeguard.

The writer used a navigation fix only while it was under a minute old by
the server's clock, and ignored every GNSS fix meanwhile. That dropped
navigation fixes delivered late, and GNSS fixes for up to a minute after
navigation stopped.

Now every navigation fix is stored at its own time, and a GNSS fix is
stored unless a navigation fix is within 15 s of it. On an R1S the
navigation stream ran at 5.0 s (p99 5.1 s), so 15 s keeps the feeds from
interleaving while letting GNSS take over as soon as navigation stops.
A late fix no longer becomes the point later moves are measured from.

Legacy now counts as a track of its own while it has three distinct
fixes within 30 s, instead of two short gaps followed by up to five
minutes of quiet. Replayed over 33 navigated R1S drives, the old rule
held navigation fixes back for 2,445 s after legacy had gone quiet; this
one never does, and a wake-up burst on an R2 holds for 30 s at most.
The held navigation fix is no longer deleted when legacy takes over.
@MrSerious0

Copy link
Copy Markdown
Contributor

Ran your PR on my R2 (build 856b399, not restarted since 2026-11-05). No error, failed or refused lines
in the log. Three drives stored a track: 407, 26 and 10 fixes, the last two sparse, and two other drives
stored one fix each. Short version: one logic bug, one smaller interaction, and a few test and comment points.

1. The density rule compares the wrong two times. legacyTracks() asks "is legacy dense?" with
navAt - times[0] <= 30_000, but "three fixes within 30 s" is times[2] - times[0]. On a feed that reports
about once a minute those three span 120 s, so a navigation fix stamped anywhere in the 30 s after times[0]
is 90 to 120 s older than the newest legacy fix, and preferLegacy reads legacy as dense and drops the
navigation fix. A stamp before times[0] gives a negative difference, which passes the same test.

That is not hypothetical. My box logged "legacy reports a dense fix stream" four times, naming three legacy
fixes that spanned 184.0, 74.1, 60.1 and 58.6 s, so none of them was dense by that definition. Three came
around a handover and the fourth about four minutes after the container started.

2. A navigation fix that was never stored still suppresses the next GPS fix. noteNavFix() runs before
write(), so a fix that write() then rejects as a step back has already opened its 15 s window
(NAV_COVERS_MS), and the next GPS fix is suppressed by navCovers. The window should not open when the
write was refused as a step back. Step-backs are not measurable from the table, since ordering by ts makes
any such query tautological, so this comes from the code order.

3. Small ones. parallax/domains/navigation.ts:175 still points at NAV_FIX_FRESH_MS, which this PR
deletes. The 15 s test's title says the opposite of what it asserts (15.000 s dropped, 15.001 s stored). The
"replayed once" test sends the same delta twice, so it cannot tell whether the dedupe works. The back-step
branch (location-writer.ts:133) has no test; that predates this PR.

On my data it cost no fix. Replaying 749 unique navigation fixes captured on 2026-11-04, delivery delay
(recv_at - stamp) was p50 2.0 s, p99 4.75 s, max 6.1 s, none over 15 s, while legacy fixes are a median
61.2 s apart. The density rule dropped none of them, as-is or with the fix. A harmful case needs a fix
delivered minutes late, which this car has not shown.

Fixes plus tests: https://github.com/MrSerious0/rivianmate/tree/fix/nav-density-window (typecheck and lint
clean; the new late-fix test fails on the PR head and passes with the fix). Happy to open a PR against
track-fix-precedence, or you can take just the density change.

blike and others added 2 commits November 6, 2026 16:28
Legacy counted as a dense track when the navigation fix was within 30 s
of the oldest of its last three fixes. On a feed reporting once a
minute those three span two minutes, so a navigation fix stamped before
the newest legacy fix read as dense and was dropped. Now only the legacy
fixes at or before the navigation fix count, and three of them must fall
in the 30 s up to it.

The feed is judged only when a delta brings a new navigation fix. The
cached one was settled when it arrived; judging it again as it aged
logged a switch of source that never happened.

A navigation fix refused for stepping the track back no longer claims
its 15 s, so it can't mute the GNSS fix that is the only point there.

The replay test now uses a new monitor, so only the stored row stops the
frame being stored twice, and checks the save doesn't fail.

Co-authored-by: Grant Leisz <MrSerious0@users.noreply.github.com>
An R2 reports its own fix about once a minute, so its marker on the
dashboard jumped once a minute even while navigation was sending a fix
every 5 s. The map now follows the rule the stored track does: the
navigation fix while one is running, and the vehicle's own fix once that
is more than 15 s newer. Speed and heading come with the fix shown, so
they match the marker. An R1, whose own fix is already a track, is
unchanged, since the server doesn't keep its navigation fixes.

A delta carrying only a navigation fix now reaches the live stream too,
so the map moves as each one arrives. The live trail skips a fix older
than the last one drawn, so it doesn't step back when navigation's fix
was taken a moment before the GPS fix already shown. The out-of-date
warning judges the fix the map shows.

Geofences, charging and drive detection still read only the vehicle's
own fix. The 15 s window moves to state-utils so the web shares it.
@blike

blike commented Nov 7, 2026 •

Copy link
Copy Markdown
Owner Author

Thanks for running it on your R2 and for the fixes. I took your branch: 475ead9 has your commit included.

On #1: agreed, and your fix is in. I think the four "dense" log lines came mostly from parallax deltas with no navigation fix in them. preferLegacy judged those against the cached navigation fix, and when that fix was older than times[0] the difference went negative. Those calls only logged, since there was no navLocation in the readings to drop. That matches your replay, where no fix was actually lost. On top of your fix, I now judge density only when a delta brings a new navigation fix. Otherwise, once the cached fix ages out of the 16 kept times, an R1 would log "using navigation fixes" after navigation ends, even though legacy is still the source.

On #2: taken as-is.

On #3: I reworked two of the tests.

  • The replay test now hands the frame to a new monitor on the same database. Changing batteryLevel did get the second delta to the writer, but the writer's in-memory seenNav check stopped it before the database was involved. The test also checks that the save doesn't fail. Without onConflictDoNothing, the key still prevents a second row, but the insert throws.
  • I checked that both new tests fail with their fix reverted and pass with it.

New since your review: 3ffa998 makes the dashboard map follow the same rule. It shows the navigation fix while one is running, and switches back to the vehicle's own fix once that's more than 15 s newer. Speed and heading come from whichever fix is shown. Deltas with only a navigation fix now go to the live stream too. On an R2 this should move the marker every 5 s while navigating, instead of once a minute.

# Conflicts:
#	web/src/features/drives/DrivesPage.tsx
blike added a commit that referenced this pull request Nov 8, 2026
Merges main, track-fix-precedence (#95) and roamer-import (#97) into trips so all three ship in one merge to main, and one Docker Hub publish.

Trips now uses the import code where Roamer moved it: isTimeZone from imports/time-zone and rangeOf from imports/plan. The import flow uses Trips' shared btn-secondary and btn-danger classes and the plural from lib/format.
@blike blike closed this pull request by merging all changes into main in 9e2baec Nov 8, 2026
@MrSerious0

Copy link
Copy Markdown
Contributor

R2 result — all three checks pass

Ran it on the R2 tonight with navigation on, on pr95-3ffa998 (the head that merged). Thanks for the
R2-specific asks — here is what each one did.

1. The planned route line draws. Yes. As soon as navigation was set, the route appeared on the
dashboard map as a dotted line under the driven trail. So trip_info does carry the polyline on an R2,
which is the one you said you'd only confirmed for trip_progress on this car.

2. The marker moves every few seconds. Yes. Two navigated drives, measured from the database
afterwards:

drive duration stored fixes p50 gap p90 gap
32 14.9 min 149 5.00 s 5.07 s
33 14.3 min 134 5.00 s 5.08 s

3. The live trail stays behind the marker. Yes — no growing gap between the newest trail point and
the marker, and nothing ahead of it.

@blike
blike deleted the track-fix-precedence branch November 10, 2026 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants