Skip to content

add -safeephemtime flag to convbin/sbp2rinex for stale-eph processing#95

Merged
denniszollo merged 9 commits into
masterfrom
dzollo/safe-ephem-time
May 7, 2026
Merged

add -safeephemtime flag to convbin/sbp2rinex for stale-eph processing#95
denniszollo merged 9 commits into
masterfrom
dzollo/safe-ephem-time

Conversation

@denniszollo
Copy link
Copy Markdown

@denniszollo denniszollo commented Apr 30, 2026

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:

  • 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 (RTCM3 MT1019/1044/1045/1046, which carry 10-bit weeks) against -tr instead of timeget()
  • Trusts the BDS 13-bit week directly in MT1042 (no rollover needed for ~140 years)
  • Traces a level-2 warning when an RTCM3 eph is >3 days from -tr

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

  • adjgpsweek_ref(week, ref) in rtkcmn.c: new helper that mirrors adjgpsweek but takes the reference time as an argument. Uses round-to-nearest centering (+512) rather than the past-biased +1 used by adjgpsweek. The legacy +1 formula gives error window (-1023, +1]
    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.
  • Gates added to MT 1019/1041/1042/1044/1045/1046 in rtcm3.c. With -SAFEEPHEMTIME, an eph received before rtcm->time is set is rejected (no timeget() fallback). Stale-eph trace threshold factored into SAFEEPHEM_STALE_THRESHOLD_S.
  • SBP eph decoders in swiftnav.c: drop adjgpsweek from decode_gpsnav_common, decode_gpsnav_common_dep1, decode_bdsnav_common, and decode_galnav_common. Common-helper signatures unchanged from upstream. Per-message decoders (decode_gpsnav, decode_gpsnav_dep_e/f,
    decode_qzssnav, decode_bdsnav, decode_galnav, decode_galnav_dep_a) gate the eph.ttr=timeget() fallback on -SAFEEPHEMTIME.
  • 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 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

  • Verified against AusPos ephemeris service data from 05/01. With -safeephemtime -tr 2026/05/01, 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 incorrect 04
    25 22 00 00 output with a false week increment is preserved.
  • A few unit tests added.

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>
@denniszollo
Copy link
Copy Markdown
Author

denniszollo commented May 1, 2026

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:

➜  sbp2rinex -os -r rtcm3 -tr 2026/05/01 0:0:0 igs_ephem_may_1.rtcm
sbp2rinex v2.6 (RTKLIB 2.4.3b34)
input file  : igs_ephem_may_1.rtcm (RTCM 3)
->rinex obs : igs_ephem_may_1.obs
->rinex nav : igs_ephem_may_1.nav
->sbas log  : igs_ephem_may_1.sbs

scanning: 2026/05/01 00:00:00
: N=539 01 00:00:00: N=539
➜  grep C45 -A7 igs_ephem_may_1.nav
C45 2026 04 25 22 00 00 -.805587973446D-03 -.207922568052D-10  .000000000000D+00
      .100000000000D+01 -.292343750000D+02  .388266172845D-08  .140628689804D+01
     -.134902074933D-05  .503214891069D-03  .109011307359D-04  .528266219902D+04
      .597600000000D+06 -.884756445885D-07 -.270409900275D+00 -.112690031528D-06
      .949702308170D+00  .137968750000D+03  .288908224359D+00 -.667599236759D-08
     -.799319009131D-09  .000000000000D+00  .105900000000D+04  .000000000000D+00
      .200000000000D+01  .000000000000D+00  .193000000000D-07  .193000000000D-07
      .103678600000D+07  .100000000000D+01

Now, using this new flag, I get the actual on-the-wire (but stale and old) ephemeris with an earlier date:

➜  sbp2rinex -os -r rtcm3 -tr 2026/05/01 0:0:0 -safeephemtime igs_ephem_may_1.rtcm
sbp2rinex v2.6 (RTKLIB 2.4.3b34)
input file  : igs_ephem_may_1.rtcm (RTCM 3)
->rinex obs : igs_ephem_may_1.obs
->rinex nav : igs_ephem_may_1.nav
->sbas log  : igs_ephem_may_1.sbs

scanning: 2026/05/01 00:00:00
: N=539 01 00:00:00: N=539
➜  skylark-networks git:(dzollo/station-selector-shared-thresholds) ✗ grep C45 -A7 igs_ephem_may_1.nav
C45 2026 04 18 22 00 00 -.805587973446D-03 -.207922568052D-10  .000000000000D+00
      .100000000000D+01 -.292343750000D+02  .388266172845D-08  .140628689804D+01
     -.134902074933D-05  .503214891069D-03  .109011307359D-04  .528266219902D+04
      .597600000000D+06 -.884756445885D-07 -.270409900275D+00 -.112690031528D-06
      .949702308170D+00  .137968750000D+03  .288908224359D+00 -.667599236759D-08
     -.799319009131D-09  .000000000000D+00  .105800000000D+04  .000000000000D+00
      .200000000000D+01  .000000000000D+00  .193000000000D-07  .193000000000D-07
      .164158600000D+07  .100000000000D+01

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.

Copy link
Copy Markdown

@jackleckert jackleckert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running a manual test on the 2026-04-25 (day of failing sqa sweep) gave GPS ephemeris times at 2006 only.

denniszollo and others added 4 commits May 5, 2026 17:10
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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (no timeget() dependency).
  • Updated RTCM3 ephemeris decoders (MT1019/1041/1042/1044/1045/1046) to support -SAFEEPHEMTIME behavior, 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 plus convbin CLI 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.

Comment thread src/rtkcmn.c
Comment thread src/rcv/swiftnav.c Outdated
Comment thread app/consapp/convbin/convbin.c Outdated
denniszollo and others added 2 commits May 6, 2026 15:43
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>
Copy link
Copy Markdown

@ebethon ebethon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@denniszollo denniszollo requested review from dgburr and ljbade May 6, 2026 20:36
Copy link
Copy Markdown

@jackleckert jackleckert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just one minor DRY comment.

Comment thread src/rtcm3.c
prn,-tt/86400.0);
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block is 6 times in this file. Can we create a function for that?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea.

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>
@denniszollo denniszollo merged commit c0820de into master May 7, 2026
5 checks passed
@denniszollo denniszollo deleted the dzollo/safe-ephem-time branch May 7, 2026 18:46
Comment thread src/rtcm3.c

/* 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. */
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)"

Comment thread src/rtcm3.c
}
/* 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. */
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not include the full URL?

" -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",
Copy link
Copy Markdown

@dgburr dgburr May 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"?

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.

5 participants