add -safeephemtime flag to convbin/sbp2rinex for stale-eph processing#95
Conversation
When decoding ephemerides from offline RTCM3/SBP captures that contain stale
ephs, RTKLIB's eph decoders silently rewrite the on-the-wire week number.
The decoders compare each eph against rtcm->time (or wall clock if unset)
and apply a +/-1 week "correction" whenever the eph appears more than 3.5
days off, which corrupts eph.week, eph.toe, and eph.toc for any eph older
than that threshold. SBP decoders separately fall back on timeget() for
rollover resolution and for eph.ttr.
This is fine for live receivers but wrong for offline replay where the user
knows the data's true epoch and wants the on-wire bits preserved -- e.g. so
they can filter out ancient ephs downstream by inspecting the week field.
Add a new convbin/sbp2rinex CLI flag -safeephemtime that, when set:
- Refuses to consult the OS wall clock anywhere on the eph path
- Refuses to mutate eph.week via the +/-1 nudge
- Uses -tr (now required) as the time reference; rolls over truncated GPS
week fields against -tr instead of timeget()
- Trusts the BDS 13-bit week directly (no rollover needed for ~140 years)
- Traces a level-2 warning when an eph is >3 days from -tr
Implementation:
- New helper adjgpsweek_ref(week, ref) in rtkcmn.c that mirrors adjgpsweek
but takes the reference time as an argument
- Gates added to MT 1019/1041/1042/1044/1045/1046 in rtcm3.c
- SBP eph helpers in swiftnav.c take raw_t* now and gate adjgpsweek and
the eph.ttr=timeget() fallback; this affects decode_gpsnav,
decode_gpsnav_dep_e/f, decode_qzssnav, decode_bdsnav, decode_galnav,
decode_galnav_dep_a
- convrnx.c seeds str->time from opt->trtcm for SBP too when the flag
is in rcvopt
- convbin.c parses -safeephemtime, validates that -tr is also supplied,
and injects -SAFEEPHEMTIME into opt.rcvopt for downstream propagation
The flag is scoped to ephemeris decoders only; observation/range decoding
is untouched. The legacy path (no flag) is preserved verbatim for back-
compat -- users who currently rely on the wall-clock-driven behavior see
no change.
Also pulls in upstream rtklibexplorer/RTKLIB@1b6036e: adjbdtweek's rollover
modulus is corrected from 1024 to 8192 to match the 13-bit on-wire BDS week
field. No-op for current data; affects only legacy-path archival decoding
of ephs >10 years older than the current week.
Verified against auspos ephemeris service data from 04/27.
With -safeephemtime -tr 2026/04/27, C45 (BDS week 1058, toe 597600s)
now produces 2026 04 18 22 00 00 BDT and eph.week=1058 in the RINEX nav file,
matching the on-wire data; without the flag the incorrrect
04 25 22 00 00 output with a false week increment is preserved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Here is a quick record of a test. Usuing the BCEP00BKG0 mountpoint (BKG broadcast ephemeris stream) from geosciences Australia (Host: ntrip.data.gnss.ga.gov.au:2101, I record 1 hour of data from Friday May 1 (attached here as .zip file). igs_ephem_may_1.rtcm.zip With the default arguments, i get the following ephemeris from C45 in the nav file: Now, using this new flag, I get the actual on-the-wire (but stale and old) ephemeris with an earlier date: Note that both the receiver/line time, AND the week number in the actual nav data (6th row, column 3 ), are incorrect with the default flags against this data without the new flag. it should be BDS week 1058, but rtklib is giving me 1059 assuming that we aren't mixing ephemerides more than 3.5 days old without this change. |
jackleckert
left a comment
There was a problem hiding this comment.
Running a manual test on the 2026-04-25 (day of failing sqa sweep) gave GPS ephemeris times at 2006 only.
Match the past-biased +1 formula in adjgpsweek is wrong for offline replay -- ephs broadcast for the next week (TOE = ref_w + 1) get rolled 1024 weeks back to the previous rollover. Centering at +512 gives a symmetric (-512, +512] error window so forward-broadcast ephs resolve to the correct era. Caveat called out in the comment: for ephs >9.8 years older than -tr, +512 rounds toward the future side. Truly archival data should use a -tr that matches the data's epoch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SBP carries a full u16 GPS week on the wire (per spec; the obs path at line 530 and the GLO eph paths already trust it directly). adjgpsweek on the SBP eph path is dead code in the common case and a latent bug for archival captures: for ephs more than ~512 weeks older than the wall clock it silently rolls them forward by 1024 weeks. Drop adjgpsweek (and the safemode adjgpsweek_ref ternary that the prior commit added). Eph week is now just _pEph->week = uWeekE unconditionally -- correct for both legacy and safemode paths. This also lets us revert the decode_*_common helper signatures back to upstream (no raw_t*, no safeephem local, no stale-trace block). The per-msg ttr gates that prevent timeget() fallback under safemode stay -- those are load-bearing for the wall-clock-free contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pull the magic number 86400.0*3.0 out into a named macro so the stale-eph trace threshold is set in one place across all six rtcm3 ephemeris decoders (MT 1019/1041/1042/1044/1045/1046). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three new subtargets, mirroring the existing test1-test6 pattern: - test_safeephem_guard: convbin must reject -safeephemtime without -tr. - test_safeephem_sbp_anchors: 2017 SBP fixture under -safeephemtime -tr 2017/05/12 produces 2017-anchored nav epochs (not pulled forward to wall-clock year). Catches regressions in the SBP full-week trust. - test_safeephem_rtcm3_anchors: 2026 IGS RTCM3 capture under -safeephemtime -tr 2026/05/01 produces 2026-anchored nav epochs. Exercises the rtcm3 MT 1042/1045/1046 paths end-to-end. Drops a truncated 4 KB slice of the IGS capture as a fixture (sufficient to surface decode regressions; full 8.5 MB is overkill for CI). Skipped: unit tests for adjgpsweek_ref / adjbdtweek -- the legacy adjgpsweek and adjbdtweek functions have no existing test coverage, and the user-visible behavior is already exercised end-to-end here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous "any 2026 epoch present" check would pass for both legacy and safemode paths since the IGS fixture data is current-week and both paths produce 2026 dates. It only caught catastrophic regressions (safemode crashes, produces wrong year), not the actual nudge bug that motivated the PR. The fixture's C45 first eph has toe 597600s at BDS week 1058 -- more than 3.5 days from the wall clock, so legacy code nudges eph.week++ producing "2026 04 25 22 00 00", while safemode preserves on-wire bits producing "2026 04 18 22 00 00". Assert the safemode-specific date. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces a new -safeephemtime mode for convbin/sbp2rinex to make ephemeris decoding deterministic for offline captures by anchoring week rollover resolution to user-provided -tr, avoiding OS wall-clock use, and preventing the legacy +/-1-week “nudge” from mutating on-wire ephemeris week fields.
Changes:
- Added
adjgpsweek_ref(week, ref)to resolve 10-bit GPS week rollovers against an explicit reference time (notimeget()dependency). - Updated RTCM3 ephemeris decoders (MT1019/1041/1042/1044/1045/1046) to support
-SAFEEPHEMTIMEbehavior, including stale-ephemeris tracing. - Updated SBP ephemeris decoding to trust on-wire week values (drop
adjgpsweek()in common helpers), propagate anchoring time, and added makefile-based regression tests plusconvbinCLI parsing/validation for-safeephemtime.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/rtklib.h |
Exposes new adjgpsweek_ref() API. |
src/rtkcmn.c |
Implements adjgpsweek_ref() for reference-time-based rollover. |
src/rtcm3.c |
Adds safemode gating for RTCM3 ephemeris week handling and stale-eph warnings; fixes BDS week modulus. |
src/rcv/swiftnav.c |
Removes adjgpsweek() from SBP eph common decoders; gates ttr wall-clock fallback in safemode. |
src/convrnx.c |
Seeds stream time from -tr when safemode token is present. |
app/consapp/convbin/gcc/makefile |
Adds regression targets for safemode behavior. |
app/consapp/convbin/convbin.c |
Adds -safeephemtime CLI flag, requires -tr, and injects -SAFEEPHEMTIME into receiver options. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
decode_bdsnav_common was constructing eph.toe via gpst2time(_pEph->week, _pEph->toes) where _pEph->week is BDT-relative (uWeekE - 1356) and _pEph->toes is BDT tow. Passing a BDT-relative week to gpst2time produces a gtime_t representing an instant 1356 weeks (~26 years) before the actual eph time -- enough to break any downstream consumer that uses eph.toe for satellite position computation or ephemeris validity windows. The RINEX-nav writer (rinex.c outrnxnavb) doesn't read eph.toe directly -- it derives the date from gpst2bdt(eph.toc) and writes eph.toes/ eph.week as scalars -- so the bug never showed up in test6's reference diff. But in-memory consumers (rtkpos, ephemeris.c) would all see a 1992-era gtime_t for a 2018 eph. Use the bdt2gpst(bdt2time(eph.week, eph.toes)) pattern that the rtcm3.c MT 1042 decoder already uses for the same conversion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
jackleckert
left a comment
There was a problem hiding this comment.
LGTM, just one minor DRY comment.
| prn,-tt/86400.0); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
This block is 6 times in this file. Can we create a function for that?
Deduplicate the SAFEEPHEMTIME stale-eph trace across the six eph decoders (1019/1041/1042/1044/1045/1046) into a single helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
||
| /* Threshold (seconds) for the safemode stale-eph trace warning. Independent | ||
| of the legacy +/-1 week nudge boundary (302400s = 3.5 days), which this | ||
| PR removes from the safemode path. */ |
There was a problem hiding this comment.
This comment only makes sense within the context of this PR, suggest changing to something like "This is only used when the -safeephemtime flag is specified, otherwise the default behaviour is to fall back to a +/-1 week nudge boundary (302400s = 3.5 days)"
| } | ||
| /* adjust weekly rollover of BDS time ----------------------------------------*/ | ||
| /* BDS WN field on the wire is 13 bits (range 0..8191); mod-8192 matches that. | ||
| See rtklibexplorer/RTKLIB@1b6036e. */ |
| " -l lfile output RINEX LNAV file", | ||
| " -s sfile output SBAS message file", | ||
| " -trace level output trace level [off]", | ||
| " -safeephemtime refuse to use wall clock and refuse to increment", |
There was a problem hiding this comment.
Why add a new command-line argument instead of just requiring the user to specify -ro SAFEEPHEMTIME (which appears to be how it is implemented internally anyway)? And does it really make sense to call this a "safe" time? I understand that in your specific context, this flag results in the desired behaviour. But for other use cases one could argue that the default behaviour is the "safest". Maybe consider renaming it to something like "NOEPHWEEKWRAP"?
Description
When decoding ephemerides from offline RTCM3/SBP captures that contain stale ephs, RTKLIB's eph decoders silently rewrite the on-the-wire week number. The decoders compare each eph against rtcm->time (or wall clock if unset) and apply a +/-1 week "correction" whenever the eph appears more than 3.5 days off, which corrupts eph.week, eph.toe, and eph.toc for any eph older than that threshold. SBP decoders separately fall back on timeget() for the rollover resolution and for eph.ttr.
This is fine for live receivers but wrong for offline replay where the user knows the data's true epoch and wants the on-wire bits preserved — e.g. so they can filter out ancient ephs downstream by inspecting the week field.
With this change, we add a new convbin/sbp2rinex CLI flag -safeephemtime that, when set:
SBP eph decoding is simplified separately. SBP carries a full 16-bit GPS week on the wire (the obs and GLO eph paths in swiftnav.c already trust it directly), so the adjgpsweek call on the GPS/QZSS/BDS/GAL eph path was dead code in the common case and a latent bug for archival captures: for ephs more than ~512 weeks older than the wall clock it silently rolled them forward by 1024 weeks. This PR removes adjgpsweek from the SBP eph path unconditionally — under both the legacy and safemode paths the decoders now use the on-wire week directly. This is a small legacy-path behavior change that only affects archival data >9.8 years older than the current wall clock; for current data it is a no-op.
Implementation
weeks, which is fine for live receivers (eph TOE ≤ current week) but breaks on offline captures whose ephs are valid into the next week. Centering via +512 gives a symmetric (-512, +512] window. Used only by RTCM3 decoders for 10-bit truncated weeks.
decode_qzssnav, decode_bdsnav, decode_galnav, decode_galnav_dep_a) gate the eph.ttr=timeget() fallback on -SAFEEPHEMTIME.
The flag is scoped to ephemeris decoders only; observation/range decoding is untouched. The legacy RTCM3 path (no flag) is preserved verbatim — users relying on the wall-clock-driven RTCM3 behavior see no change. The legacy SBP path is changed only to drop the
adjgpsweek call, which is correct for SBP's full-width on-wire week.
Also pulls in upstream rtklibexplorer/RTKLIB@1b6036e: adjbdtweek's rollover modulus is corrected from 1024 to 8192 to match the 13-bit on-wire BDS week field. No-op for current data; affects only legacy-path archival decoding of ephs >10 years older than the current
week.
Verification
25 22 00 00 output with a false week increment is preserved.