From 4d108511502bbeb41f0e9391e88a346283ad33eb Mon Sep 17 00:00:00 2001 From: Dennis Zollo Date: Thu, 30 Apr 2026 11:00:34 -0400 Subject: [PATCH 1/9] add -safeephemtime flag to convbin/sbp2rinex for stale-eph processing 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 --- app/consapp/convbin/convbin.c | 37 +++++-- src/convrnx.c | 3 +- src/rcv/swiftnav.c | 95 +++++++++++++----- src/rtcm3.c | 182 ++++++++++++++++++++++++++-------- src/rtkcmn.c | 16 +++ src/rtklib.h | 1 + 6 files changed, 259 insertions(+), 75 deletions(-) diff --git a/app/consapp/convbin/convbin.c b/app/consapp/convbin/convbin.c index 6e11b4be9..3a463f98c 100644 --- a/app/consapp/convbin/convbin.c +++ b/app/consapp/convbin/convbin.c @@ -114,6 +114,9 @@ static const char *help[]={ " -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", +" ephemeris week numbers when decoding ephemerides;", +" requires -tr; affects RTCM3 and SBP eph decoders", "", " If not any output file is specified, default output files .obs,", " .nav, .gnav, .hnav, .qnav, .lnav and", @@ -337,7 +340,8 @@ static int get_filetime(const char *file, gtime_t *time) } /* parse command line options ------------------------------------------------*/ static int cmdopts(int argc, char **argv, rnxopt_t *opt, char **ifile, - char **ofile, char **dir, int *trace) + char **ofile, char **dir, int *trace, int *safeephemtime, + int *has_tr) { double eps[]={1980,1,1,0,0,0},epe[]={2037,12,31,0,0,0}; double epr[]={2010,1,1,0,0,0},span=0.0; @@ -365,6 +369,7 @@ static int cmdopts(int argc, char **argv, rnxopt_t *opt, char **ifile, sscanf(argv[++i],"%lf/%lf/%lf",epr,epr+1,epr+2); sscanf(argv[++i],"%lf:%lf:%lf",epr+3,epr+4,epr+5); opt->trtcm=epoch2time(epr); + *has_tr=1; } else if (!strcmp(argv[i],"-ti")&&i+1tint=atof(argv[++i]); @@ -485,6 +490,9 @@ static int cmdopts(int argc, char **argv, rnxopt_t *opt, char **ifile, else if (!strcmp(argv[i],"-trace" )&&i+10)?" -SAFEEPHEMTIME":"-SAFEEPHEMTIME"; + toklen=strlen(tok); + if (used+toklen+1>sizeof(opt.rcvopt)) { + fprintf(stderr,"-ro string too long to add -SAFEEPHEMTIME\n"); + return -1; + } + memcpy(opt.rcvopt+used,tok,toklen+1); + } + if (trace>0) { traceopen(TRACEFILE); tracelevel(trace); diff --git a/src/convrnx.c b/src/convrnx.c index 5abb49526..a86100c15 100644 --- a/src/convrnx.c +++ b/src/convrnx.c @@ -1275,7 +1275,8 @@ static int convrnx_s(int sess, int format, rnxopt_t *opt, const char *file, for (i=0;ircvopt,"-SAFEEPHEMTIME")) { str->time=opt->trtcm; } else if (opt->ts.time) { diff --git a/src/rcv/swiftnav.c b/src/rcv/swiftnav.c index cf1572eab..6fb676b46 100644 --- a/src/rcv/swiftnav.c +++ b/src/rcv/swiftnav.c @@ -628,9 +628,11 @@ static int decode_msgobs(raw_t *raw) { /* common part of GPS eph decoding (navigation data) * --------------------------*/ -static void decode_gpsnav_common_dep1(uint8_t *_pBuff, eph_t *_pEph) { +static void decode_gpsnav_common_dep1(raw_t *raw, uint8_t *_pBuff, + eph_t *_pEph) { uint16_t uWeekE, uWeekC; double dToc; + int safeephem = strstr(raw->opt, "-SAFEEPHEMTIME") != NULL; _pEph->toes = U4(_pBuff + 4); uWeekE = U2(_pBuff + 8); @@ -666,16 +668,25 @@ static void decode_gpsnav_common_dep1(uint8_t *_pBuff, eph_t *_pEph) { _pEph->iode = U1(_pBuff + 182); _pEph->iodc = U2(_pBuff + 183); - _pEph->week = adjgpsweek(uWeekE); + _pEph->week = safeephem ? adjgpsweek_ref(uWeekE, raw->time) + : adjgpsweek(uWeekE); _pEph->toe = gpst2time(_pEph->week, _pEph->toes); _pEph->toc = gpst2time(uWeekC, dToc); + if (safeephem) { + double tt = timediff(_pEph->toe, raw->time); + if (fabs(tt) > 86400.0 * 3.0) { + trace(2, "sbp gpsnav -SAFEEPHEMTIME: stale eph age=%.1f days\n", + -tt / 86400.0); + } + } } /* common part of GPS eph decoding (navigation data) * --------------------------*/ -static void decode_gpsnav_common(uint8_t *_pBuff, eph_t *_pEph) { +static void decode_gpsnav_common(raw_t *raw, uint8_t *_pBuff, eph_t *_pEph) { uint16_t uWeekE, uWeekC; double dToc; + int safeephem = strstr(raw->opt, "-SAFEEPHEMTIME") != NULL; _pEph->toes = U4(_pBuff + 4); uWeekE = U2(_pBuff + 8); @@ -711,17 +722,26 @@ static void decode_gpsnav_common(uint8_t *_pBuff, eph_t *_pEph) { _pEph->iode = U1(_pBuff + 138); _pEph->iodc = U2(_pBuff + 139); - _pEph->week = adjgpsweek(uWeekE); + _pEph->week = safeephem ? adjgpsweek_ref(uWeekE, raw->time) + : adjgpsweek(uWeekE); _pEph->code = 2; /* SBP payload does not have the "code on L2" flag */ _pEph->toe = gpst2time(_pEph->week, _pEph->toes); _pEph->toc = gpst2time(uWeekC, dToc); + if (safeephem) { + double tt = timediff(_pEph->toe, raw->time); + if (fabs(tt) > 86400.0 * 3.0) { + trace(2, "sbp gpsnav -SAFEEPHEMTIME: stale eph age=%.1f days\n", + -tt / 86400.0); + } + } } /* common part of BDS eph decoding (navigation data) * --------------------------*/ -static void decode_bdsnav_common(uint8_t *_pBuff, eph_t *_pEph) { +static void decode_bdsnav_common(raw_t *raw, uint8_t *_pBuff, eph_t *_pEph) { uint16_t uWeekE, uWeekC; double dToc; + int safeephem = strstr(raw->opt, "-SAFEEPHEMTIME") != NULL; _pEph->toes = U4(_pBuff + 4) - BDS_SECOND_TO_GPS_SECOND; uWeekE = U2(_pBuff + 8); @@ -757,16 +777,25 @@ static void decode_bdsnav_common(uint8_t *_pBuff, eph_t *_pEph) { _pEph->iode = U1(_pBuff + 146); _pEph->iodc = U2(_pBuff + 147); - _pEph->week = adjgpsweek(uWeekE) - BDS_WEEK_TO_GPS_WEEK; + _pEph->week = (safeephem ? adjgpsweek_ref(uWeekE, raw->time) + : adjgpsweek(uWeekE)) - BDS_WEEK_TO_GPS_WEEK; _pEph->toe = gpst2time(_pEph->week, _pEph->toes); _pEph->toc = gpst2time(uWeekC, dToc); + if (safeephem) { + double tt = timediff(_pEph->toe, raw->time); + if (fabs(tt) > 86400.0 * 3.0) { + trace(2, "sbp bdsnav -SAFEEPHEMTIME: stale eph age=%.1f days\n", + -tt / 86400.0); + } + } } /* common part of GAL eph decoding (navigation data) * --------------------------*/ -static void decode_galnav_common(uint8_t *_pBuff, eph_t *_pEph) { +static void decode_galnav_common(raw_t *raw, uint8_t *_pBuff, eph_t *_pEph) { uint16_t uWeekE, uWeekC; double dToc; + int safeephem = strstr(raw->opt, "-SAFEEPHEMTIME") != NULL; _pEph->toes = U4(_pBuff + 4); uWeekE = U2(_pBuff + 8); @@ -802,9 +831,17 @@ static void decode_galnav_common(uint8_t *_pBuff, eph_t *_pEph) { _pEph->iode = U2(_pBuff + 150); _pEph->iodc = U2(_pBuff + 152); - _pEph->week = adjgpsweek(uWeekE); + _pEph->week = safeephem ? adjgpsweek_ref(uWeekE, raw->time) + : adjgpsweek(uWeekE); _pEph->toe = gpst2time(_pEph->week, _pEph->toes); _pEph->toc = gpst2time(uWeekC, dToc); + if (safeephem) { + double tt = timediff(_pEph->toe, raw->time); + if (fabs(tt) > 86400.0 * 3.0) { + trace(2, "sbp galnav -SAFEEPHEMTIME: stale eph age=%.1f days\n", + -tt / 86400.0); + } + } } /* decode deprecated SBP nav message for GPS (navigation data) @@ -833,12 +870,13 @@ static int decode_gpsnav_dep_e(raw_t *raw) { eph.code = U1(puiTmp + 2); - decode_gpsnav_common_dep1(puiTmp, &eph); + decode_gpsnav_common_dep1(raw, puiTmp, &eph); - if (0 == timediff(raw->time, time0)) { - eph.ttr = timeget(); - } else { + if (strstr(raw->opt, "-SAFEEPHEMTIME") || + timediff(raw->time, time0) != 0) { eph.ttr = raw->time; + } else { + eph.ttr = timeget(); } if (!strstr(raw->opt, "EPHALL")) { @@ -895,12 +933,13 @@ static int decode_gpsnav_dep_f(raw_t *raw) { return -1; } - decode_gpsnav_common_dep1(puiTmp - 2, &eph); + decode_gpsnav_common_dep1(raw, puiTmp - 2, &eph); - if (0 == timediff(raw->time, time0)) { - eph.ttr = timeget(); - } else { + if (strstr(raw->opt, "-SAFEEPHEMTIME") || + timediff(raw->time, time0) != 0) { eph.ttr = raw->time; + } else { + eph.ttr = timeget(); } if (!strstr(raw->opt, "EPHALL")) { @@ -950,12 +989,13 @@ static int decode_gpsnav(raw_t *raw) { return -1; } - decode_gpsnav_common(puiTmp - 2, &eph); + decode_gpsnav_common(raw, puiTmp - 2, &eph); - if (0 == timediff(raw->time, time0)) { - eph.ttr = timeget(); - } else { + if (strstr(raw->opt, "-SAFEEPHEMTIME") || + timediff(raw->time, time0) != 0) { eph.ttr = raw->time; + } else { + eph.ttr = timeget(); } if (!strstr(raw->opt, "EPHALL")) { @@ -1013,12 +1053,13 @@ static int decode_qzssnav(raw_t *raw) { return -1; } - decode_gpsnav_common(puiTmp - 2, &eph); + decode_gpsnav_common(raw, puiTmp - 2, &eph); - if (0 == timediff(raw->time, time0)) { - eph.ttr = timeget(); - } else { + if (strstr(raw->opt, "-SAFEEPHEMTIME") || + timediff(raw->time, time0) != 0) { eph.ttr = raw->time; + } else { + eph.ttr = timeget(); } if (!strstr(raw->opt, "EPHALL")) { @@ -1076,7 +1117,7 @@ static int decode_bdsnav(raw_t *raw) { return -1; } - decode_bdsnav_common(puiTmp - 2, &eph); + decode_bdsnav_common(raw, puiTmp - 2, &eph); eph.ttr = raw->time; @@ -1128,7 +1169,7 @@ static int decode_galnav_dep_a(raw_t *raw) { return -1; } - decode_galnav_common(puiTmp - 2, &eph); + decode_galnav_common(raw, puiTmp - 2, &eph); eph.ttr = raw->time; @@ -1180,7 +1221,7 @@ static int decode_galnav(raw_t *raw) { return -1; } - decode_galnav_common(puiTmp - 2, &eph); + decode_galnav_common(raw, puiTmp - 2, &eph); eph.ttr = raw->time; diff --git a/src/rtcm3.c b/src/rtcm3.c index 6446e450f..a0836775d 100644 --- a/src/rtcm3.c +++ b/src/rtcm3.c @@ -181,12 +181,14 @@ static void adjweek(rtcm_t *rtcm, double tow) rtcm->time=gpst2time(week,tow); } /* 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. */ static int adjbdtweek(int week) { int w; (void)time2bdt(gpst2bdt(utc2gpst(timeget())),&w); if (w<1) w=1; /* use 2006/1/1 if time is earlier than 2006/1/1 */ - return week+(w-week+512)/1024*1024; + return week+(w-week+4095)/8192*8192; } /* adjust daily rollover of GLONASS time -------------------------------------*/ static void adjday_glot(rtcm_t *rtcm, double tod) @@ -796,13 +798,29 @@ static int decode_type1019(rtcm_t *rtcm) return -1; } eph.sat=sat; - eph.week=adjgpsweek(week); - if (rtcm->time.time==0) rtcm->time=utc2gpst(timeget()); - tt=timediff(gpst2time(eph.week,eph.toes),rtcm->time); - if (tt<-302400.0) eph.week++; - else if (tt>=302400.0) eph.week--; - eph.toe=gpst2time(eph.week,eph.toes); - eph.toc=gpst2time(eph.week,toc); + { + int safeephem=strstr(rtcm->opt,"-SAFEEPHEMTIME")!=NULL; + if (safeephem&&rtcm->time.time==0) { + trace(2,"rtcm3 1019 -SAFEEPHEMTIME: rtcm->time not set; rejecting eph\n"); + return -1; + } + eph.week=safeephem?adjgpsweek_ref(week,rtcm->time):adjgpsweek(week); + if (!safeephem) { + if (rtcm->time.time==0) rtcm->time=utc2gpst(timeget()); + tt=timediff(gpst2time(eph.week,eph.toes),rtcm->time); + if (tt<-302400.0) eph.week++; + else if (tt>=302400.0) eph.week--; + } + eph.toe=gpst2time(eph.week,eph.toes); + eph.toc=gpst2time(eph.week,toc); + if (safeephem) { + tt=timediff(eph.toe,rtcm->time); + if (fabs(tt)>86400.0*3.0) { + trace(2,"rtcm3 1019 -SAFEEPHEMTIME: stale eph prn=%d age=%.1f days\n", + prn,-tt/86400.0); + } + } + } eph.ttr=rtcm->time; eph.A=sqrtA*sqrtA; if (!strstr(rtcm->opt,"-EPHALL")) { @@ -1113,13 +1131,29 @@ static int decode_type1041(rtcm_t *rtcm) return -1; } eph.sat=sat; - eph.week=adjgpsweek(week); - if (rtcm->time.time==0) rtcm->time=utc2gpst(timeget()); - tt=timediff(gpst2time(eph.week,eph.toes),rtcm->time); - if (tt<-302400.0) eph.week++; - else if (tt>=302400.0) eph.week--; - eph.toe=gpst2time(eph.week,eph.toes); - eph.toc=gpst2time(eph.week,toc); + { + int safeephem=strstr(rtcm->opt,"-SAFEEPHEMTIME")!=NULL; + if (safeephem&&rtcm->time.time==0) { + trace(2,"rtcm3 1041 -SAFEEPHEMTIME: rtcm->time not set; rejecting eph\n"); + return -1; + } + eph.week=safeephem?adjgpsweek_ref(week,rtcm->time):adjgpsweek(week); + if (!safeephem) { + if (rtcm->time.time==0) rtcm->time=utc2gpst(timeget()); + tt=timediff(gpst2time(eph.week,eph.toes),rtcm->time); + if (tt<-302400.0) eph.week++; + else if (tt>=302400.0) eph.week--; + } + eph.toe=gpst2time(eph.week,eph.toes); + eph.toc=gpst2time(eph.week,toc); + if (safeephem) { + tt=timediff(eph.toe,rtcm->time); + if (fabs(tt)>86400.0*3.0) { + trace(2,"rtcm3 1041 -SAFEEPHEMTIME: stale eph prn=%d age=%.1f days\n", + prn,-tt/86400.0); + } + } + } eph.ttr=rtcm->time; eph.A=sqrtA*sqrtA; eph.iodc=eph.iode; @@ -1186,13 +1220,29 @@ static int decode_type1044(rtcm_t *rtcm) return -1; } eph.sat=sat; - eph.week=adjgpsweek(week); - if (rtcm->time.time==0) rtcm->time=utc2gpst(timeget()); - tt=timediff(gpst2time(eph.week,eph.toes),rtcm->time); - if (tt<-302400.0) eph.week++; - else if (tt>=302400.0) eph.week--; - eph.toe=gpst2time(eph.week,eph.toes); - eph.toc=gpst2time(eph.week,toc); + { + int safeephem=strstr(rtcm->opt,"-SAFEEPHEMTIME")!=NULL; + if (safeephem&&rtcm->time.time==0) { + trace(2,"rtcm3 1044 -SAFEEPHEMTIME: rtcm->time not set; rejecting eph\n"); + return -1; + } + eph.week=safeephem?adjgpsweek_ref(week,rtcm->time):adjgpsweek(week); + if (!safeephem) { + if (rtcm->time.time==0) rtcm->time=utc2gpst(timeget()); + tt=timediff(gpst2time(eph.week,eph.toes),rtcm->time); + if (tt<-302400.0) eph.week++; + else if (tt>=302400.0) eph.week--; + } + eph.toe=gpst2time(eph.week,eph.toes); + eph.toc=gpst2time(eph.week,toc); + if (safeephem) { + tt=timediff(eph.toe,rtcm->time); + if (fabs(tt)>86400.0*3.0) { + trace(2,"rtcm3 1044 -SAFEEPHEMTIME: stale eph prn=%d age=%.1f days\n", + prn,-tt/86400.0); + } + } + } eph.ttr=rtcm->time; eph.A=sqrtA*sqrtA; eph.flag=1; /* fixed to 1 */ @@ -1265,12 +1315,28 @@ static int decode_type1045(rtcm_t *rtcm) } eph.sat=sat; eph.week=week+1024; /* gal-week = gst-week + 1024 */ - if (rtcm->time.time==0) rtcm->time=utc2gpst(timeget()); - tt=timediff(gpst2time(eph.week,eph.toes),rtcm->time); - if (tt<-302400.0) eph.week++; - else if (tt>=302400.0) eph.week--; - eph.toe=gpst2time(eph.week,eph.toes); - eph.toc=gpst2time(eph.week,toc); + { + int safeephem=strstr(rtcm->opt,"-SAFEEPHEMTIME")!=NULL; + if (safeephem&&rtcm->time.time==0) { + trace(2,"rtcm3 1045 -SAFEEPHEMTIME: rtcm->time not set; rejecting eph\n"); + return -1; + } + if (!safeephem) { + if (rtcm->time.time==0) rtcm->time=utc2gpst(timeget()); + tt=timediff(gpst2time(eph.week,eph.toes),rtcm->time); + if (tt<-302400.0) eph.week++; + else if (tt>=302400.0) eph.week--; + } + eph.toe=gpst2time(eph.week,eph.toes); + eph.toc=gpst2time(eph.week,toc); + if (safeephem) { + tt=timediff(eph.toe,rtcm->time); + if (fabs(tt)>86400.0*3.0) { + trace(2,"rtcm3 1045 -SAFEEPHEMTIME: stale eph prn=%d age=%.1f days\n", + prn,-tt/86400.0); + } + } + } eph.ttr=rtcm->time; eph.A=sqrtA*sqrtA; eph.svh=(e5a_hs<<4)+(e5a_dvs<<3); @@ -1346,12 +1412,28 @@ static int decode_type1046(rtcm_t *rtcm) } eph.sat=sat; eph.week=week+1024; /* gal-week = gst-week + 1024 */ - if (rtcm->time.time==0) rtcm->time=utc2gpst(timeget()); - tt=timediff(gpst2time(eph.week,eph.toes),rtcm->time); - if (tt<-302400.0) eph.week++; - else if (tt>=302400.0) eph.week--; - eph.toe=gpst2time(eph.week,eph.toes); - eph.toc=gpst2time(eph.week,toc); + { + int safeephem=strstr(rtcm->opt,"-SAFEEPHEMTIME")!=NULL; + if (safeephem&&rtcm->time.time==0) { + trace(2,"rtcm3 1046 -SAFEEPHEMTIME: rtcm->time not set; rejecting eph\n"); + return -1; + } + if (!safeephem) { + if (rtcm->time.time==0) rtcm->time=utc2gpst(timeget()); + tt=timediff(gpst2time(eph.week,eph.toes),rtcm->time); + if (tt<-302400.0) eph.week++; + else if (tt>=302400.0) eph.week--; + } + eph.toe=gpst2time(eph.week,eph.toes); + eph.toc=gpst2time(eph.week,toc); + if (safeephem) { + tt=timediff(eph.toe,rtcm->time); + if (fabs(tt)>86400.0*3.0) { + trace(2,"rtcm3 1046 -SAFEEPHEMTIME: stale eph prn=%d age=%.1f days\n", + prn,-tt/86400.0); + } + } + } eph.ttr=rtcm->time; eph.A=sqrtA*sqrtA; eph.svh=(e5b_hs<<7)+(e5b_dvs<<6)+(e1_hs<<1)+(e1_dvs<<0); @@ -1419,13 +1501,31 @@ static int decode_type1042(rtcm_t *rtcm) return -1; } eph.sat=sat; - eph.week=adjbdtweek(week); - if (rtcm->time.time==0) rtcm->time=utc2gpst(timeget()); - tt=timediff(bdt2gpst(bdt2time(eph.week,eph.toes)),rtcm->time); - if (tt<-302400.0) eph.week++; - else if (tt>=302400.0) eph.week--; - eph.toe=bdt2gpst(bdt2time(eph.week,eph.toes)); /* bdt -> gpst */ - eph.toc=bdt2gpst(bdt2time(eph.week,toc)); /* bdt -> gpst */ + { + int safeephem=strstr(rtcm->opt,"-SAFEEPHEMTIME")!=NULL; + if (safeephem&&rtcm->time.time==0) { + trace(2,"rtcm3 1042 -SAFEEPHEMTIME: rtcm->time not set; rejecting eph\n"); + return -1; + } + /* BDS WN in MT1042 is 13 bits, unambiguous for ~140 years; under + -SAFEEPHEMTIME use it directly without rollover resolution. */ + eph.week=safeephem?week:adjbdtweek(week); + if (!safeephem) { + if (rtcm->time.time==0) rtcm->time=utc2gpst(timeget()); + tt=timediff(bdt2gpst(bdt2time(eph.week,eph.toes)),rtcm->time); + if (tt<-302400.0) eph.week++; + else if (tt>=302400.0) eph.week--; + } + eph.toe=bdt2gpst(bdt2time(eph.week,eph.toes)); /* bdt -> gpst */ + eph.toc=bdt2gpst(bdt2time(eph.week,toc)); /* bdt -> gpst */ + if (safeephem) { + tt=timediff(eph.toe,rtcm->time); + if (fabs(tt)>86400.0*3.0) { + trace(2,"rtcm3 1042 -SAFEEPHEMTIME: stale eph prn=%d age=%.1f days\n", + prn,-tt/86400.0); + } + } + } eph.ttr=rtcm->time; eph.A=sqrtA*sqrtA; if (!strstr(rtcm->opt,"-EPHALL")) { diff --git a/src/rtkcmn.c b/src/rtkcmn.c index 9a113be1e..30f810608 100644 --- a/src/rtkcmn.c +++ b/src/rtkcmn.c @@ -1840,6 +1840,22 @@ extern int adjgpsweek(int week) if (w<1560) w=1560; /* use 2009/12/1 if time is earlier than 2009/12/1 */ return week+(w-week+1)/1024*1024; } +/* adjust 10-bit gps week using a caller-supplied reference time -------------- +* same rollover formula as adjgpsweek but takes the reference time explicitly +* so callers can avoid timeget(). intended for offline replay where the user +* supplies a known epoch (e.g. -tr) and wall clock must not be consulted. +*-----------------------------------------------------------------------------*/ +extern int adjgpsweek_ref(int week, gtime_t ref) +{ + int w,adj; + (void)time2gpst(ref,&w); + adj=(w-week+1)/1024; + if (adj!=0) { + trace(3,"adjgpsweek_ref: rolled raw=%d -> full=%d (ref_w=%d)\n", + week,week+adj*1024,w); + } + return week+adj*1024; +} /* get tick time --------------------------------------------------------------- * get current tick in ms * args : none diff --git a/src/rtklib.h b/src/rtklib.h index 87d28f830..5dc50f4c9 100644 --- a/src/rtklib.h +++ b/src/rtklib.h @@ -1382,6 +1382,7 @@ EXPORT double utc2gmst (gtime_t t, double ut1_utc); EXPORT int read_leaps(const char *file); EXPORT int adjgpsweek(int week); +EXPORT int adjgpsweek_ref(int week, gtime_t ref); EXPORT uint32_t tickget(void); EXPORT void sleepms(int ms); From dd83e1a96be2f481cb066e59e32d67321f275894 Mon Sep 17 00:00:00 2001 From: Dennis Zollo Date: Tue, 5 May 2026 17:10:09 -0400 Subject: [PATCH 2/9] adjgpsweek_ref: center rollover at +512 instead of +1 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) --- src/rtkcmn.c | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/rtkcmn.c b/src/rtkcmn.c index 30f810608..092ed83b4 100644 --- a/src/rtkcmn.c +++ b/src/rtkcmn.c @@ -1841,15 +1841,25 @@ extern int adjgpsweek(int week) return week+(w-week+1)/1024*1024; } /* adjust 10-bit gps week using a caller-supplied reference time -------------- -* same rollover formula as adjgpsweek but takes the reference time explicitly -* so callers can avoid timeget(). intended for offline replay where the user -* supplies a known epoch (e.g. -tr) and wall clock must not be consulted. +* takes the reference time explicitly so callers can avoid timeget(). intended +* for offline replay where the user supplies a known epoch (e.g. -tr). +* +* 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 +* -- fine for live receivers (eph TOE <= current week) but breaks on offline +* captures whose ephs are valid into the *next* week: a 10-bit field of +* ((ref_w + 1) mod 1024 + 1) gets rolled 1024 weeks back to the previous +* rollover (e.g. ref_w=2415, input=369 -> 1393 = year 2006) instead of +* forward by one week. Centering via +512 gives error window (-512, +512], +* which handles forward-broadcast ephs symmetrically. Caveat: for ephs +* >9.8 years older than -tr, +512 rounds toward the future side; callers +* with truly archival data should set -tr to match the data's epoch. *-----------------------------------------------------------------------------*/ extern int adjgpsweek_ref(int week, gtime_t ref) { int w,adj; (void)time2gpst(ref,&w); - adj=(w-week+1)/1024; + adj=(w-week+512)/1024; if (adj!=0) { trace(3,"adjgpsweek_ref: rolled raw=%d -> full=%d (ref_w=%d)\n", week,week+adj*1024,w); From 4bc78edc5054a2d04abd447fc0590a2a3d01b1d8 Mon Sep 17 00:00:00 2001 From: Dennis Zollo Date: Tue, 5 May 2026 17:33:23 -0400 Subject: [PATCH 3/9] swiftnav: trust SBP full-width 16-bit week directly 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) --- src/rcv/swiftnav.c | 72 +++++++++++++--------------------------------- 1 file changed, 20 insertions(+), 52 deletions(-) diff --git a/src/rcv/swiftnav.c b/src/rcv/swiftnav.c index 6fb676b46..9bd191c57 100644 --- a/src/rcv/swiftnav.c +++ b/src/rcv/swiftnav.c @@ -628,11 +628,9 @@ static int decode_msgobs(raw_t *raw) { /* common part of GPS eph decoding (navigation data) * --------------------------*/ -static void decode_gpsnav_common_dep1(raw_t *raw, uint8_t *_pBuff, - eph_t *_pEph) { +static void decode_gpsnav_common_dep1(uint8_t *_pBuff, eph_t *_pEph) { uint16_t uWeekE, uWeekC; double dToc; - int safeephem = strstr(raw->opt, "-SAFEEPHEMTIME") != NULL; _pEph->toes = U4(_pBuff + 4); uWeekE = U2(_pBuff + 8); @@ -668,25 +666,17 @@ static void decode_gpsnav_common_dep1(raw_t *raw, uint8_t *_pBuff, _pEph->iode = U1(_pBuff + 182); _pEph->iodc = U2(_pBuff + 183); - _pEph->week = safeephem ? adjgpsweek_ref(uWeekE, raw->time) - : adjgpsweek(uWeekE); + /* SBP carries a full 16-bit GPS week; trust it directly. */ + _pEph->week = uWeekE; _pEph->toe = gpst2time(_pEph->week, _pEph->toes); _pEph->toc = gpst2time(uWeekC, dToc); - if (safeephem) { - double tt = timediff(_pEph->toe, raw->time); - if (fabs(tt) > 86400.0 * 3.0) { - trace(2, "sbp gpsnav -SAFEEPHEMTIME: stale eph age=%.1f days\n", - -tt / 86400.0); - } - } } /* common part of GPS eph decoding (navigation data) * --------------------------*/ -static void decode_gpsnav_common(raw_t *raw, uint8_t *_pBuff, eph_t *_pEph) { +static void decode_gpsnav_common(uint8_t *_pBuff, eph_t *_pEph) { uint16_t uWeekE, uWeekC; double dToc; - int safeephem = strstr(raw->opt, "-SAFEEPHEMTIME") != NULL; _pEph->toes = U4(_pBuff + 4); uWeekE = U2(_pBuff + 8); @@ -722,26 +712,18 @@ static void decode_gpsnav_common(raw_t *raw, uint8_t *_pBuff, eph_t *_pEph) { _pEph->iode = U1(_pBuff + 138); _pEph->iodc = U2(_pBuff + 139); - _pEph->week = safeephem ? adjgpsweek_ref(uWeekE, raw->time) - : adjgpsweek(uWeekE); + /* SBP carries a full 16-bit GPS week; trust it directly. */ + _pEph->week = uWeekE; _pEph->code = 2; /* SBP payload does not have the "code on L2" flag */ _pEph->toe = gpst2time(_pEph->week, _pEph->toes); _pEph->toc = gpst2time(uWeekC, dToc); - if (safeephem) { - double tt = timediff(_pEph->toe, raw->time); - if (fabs(tt) > 86400.0 * 3.0) { - trace(2, "sbp gpsnav -SAFEEPHEMTIME: stale eph age=%.1f days\n", - -tt / 86400.0); - } - } } /* common part of BDS eph decoding (navigation data) * --------------------------*/ -static void decode_bdsnav_common(raw_t *raw, uint8_t *_pBuff, eph_t *_pEph) { +static void decode_bdsnav_common(uint8_t *_pBuff, eph_t *_pEph) { uint16_t uWeekE, uWeekC; double dToc; - int safeephem = strstr(raw->opt, "-SAFEEPHEMTIME") != NULL; _pEph->toes = U4(_pBuff + 4) - BDS_SECOND_TO_GPS_SECOND; uWeekE = U2(_pBuff + 8); @@ -777,25 +759,18 @@ static void decode_bdsnav_common(raw_t *raw, uint8_t *_pBuff, eph_t *_pEph) { _pEph->iode = U1(_pBuff + 146); _pEph->iodc = U2(_pBuff + 147); - _pEph->week = (safeephem ? adjgpsweek_ref(uWeekE, raw->time) - : adjgpsweek(uWeekE)) - BDS_WEEK_TO_GPS_WEEK; + /* SBP carries a full 16-bit GPS week; trust it directly. The internal + rtklib BDS convention stores eph.week as BDS-relative. */ + _pEph->week = uWeekE - BDS_WEEK_TO_GPS_WEEK; _pEph->toe = gpst2time(_pEph->week, _pEph->toes); _pEph->toc = gpst2time(uWeekC, dToc); - if (safeephem) { - double tt = timediff(_pEph->toe, raw->time); - if (fabs(tt) > 86400.0 * 3.0) { - trace(2, "sbp bdsnav -SAFEEPHEMTIME: stale eph age=%.1f days\n", - -tt / 86400.0); - } - } } /* common part of GAL eph decoding (navigation data) * --------------------------*/ -static void decode_galnav_common(raw_t *raw, uint8_t *_pBuff, eph_t *_pEph) { +static void decode_galnav_common(uint8_t *_pBuff, eph_t *_pEph) { uint16_t uWeekE, uWeekC; double dToc; - int safeephem = strstr(raw->opt, "-SAFEEPHEMTIME") != NULL; _pEph->toes = U4(_pBuff + 4); uWeekE = U2(_pBuff + 8); @@ -831,17 +806,10 @@ static void decode_galnav_common(raw_t *raw, uint8_t *_pBuff, eph_t *_pEph) { _pEph->iode = U2(_pBuff + 150); _pEph->iodc = U2(_pBuff + 152); - _pEph->week = safeephem ? adjgpsweek_ref(uWeekE, raw->time) - : adjgpsweek(uWeekE); + /* SBP carries a full 16-bit GPS week; trust it directly. */ + _pEph->week = uWeekE; _pEph->toe = gpst2time(_pEph->week, _pEph->toes); _pEph->toc = gpst2time(uWeekC, dToc); - if (safeephem) { - double tt = timediff(_pEph->toe, raw->time); - if (fabs(tt) > 86400.0 * 3.0) { - trace(2, "sbp galnav -SAFEEPHEMTIME: stale eph age=%.1f days\n", - -tt / 86400.0); - } - } } /* decode deprecated SBP nav message for GPS (navigation data) @@ -870,7 +838,7 @@ static int decode_gpsnav_dep_e(raw_t *raw) { eph.code = U1(puiTmp + 2); - decode_gpsnav_common_dep1(raw, puiTmp, &eph); + decode_gpsnav_common_dep1(puiTmp, &eph); if (strstr(raw->opt, "-SAFEEPHEMTIME") || timediff(raw->time, time0) != 0) { @@ -933,7 +901,7 @@ static int decode_gpsnav_dep_f(raw_t *raw) { return -1; } - decode_gpsnav_common_dep1(raw, puiTmp - 2, &eph); + decode_gpsnav_common_dep1(puiTmp - 2, &eph); if (strstr(raw->opt, "-SAFEEPHEMTIME") || timediff(raw->time, time0) != 0) { @@ -989,7 +957,7 @@ static int decode_gpsnav(raw_t *raw) { return -1; } - decode_gpsnav_common(raw, puiTmp - 2, &eph); + decode_gpsnav_common(puiTmp - 2, &eph); if (strstr(raw->opt, "-SAFEEPHEMTIME") || timediff(raw->time, time0) != 0) { @@ -1053,7 +1021,7 @@ static int decode_qzssnav(raw_t *raw) { return -1; } - decode_gpsnav_common(raw, puiTmp - 2, &eph); + decode_gpsnav_common(puiTmp - 2, &eph); if (strstr(raw->opt, "-SAFEEPHEMTIME") || timediff(raw->time, time0) != 0) { @@ -1117,7 +1085,7 @@ static int decode_bdsnav(raw_t *raw) { return -1; } - decode_bdsnav_common(raw, puiTmp - 2, &eph); + decode_bdsnav_common(puiTmp - 2, &eph); eph.ttr = raw->time; @@ -1169,7 +1137,7 @@ static int decode_galnav_dep_a(raw_t *raw) { return -1; } - decode_galnav_common(raw, puiTmp - 2, &eph); + decode_galnav_common(puiTmp - 2, &eph); eph.ttr = raw->time; @@ -1221,7 +1189,7 @@ static int decode_galnav(raw_t *raw) { return -1; } - decode_galnav_common(raw, puiTmp - 2, &eph); + decode_galnav_common(puiTmp - 2, &eph); eph.ttr = raw->time; From 30b5e31b107c1062c74681864a827908e4e16d3f Mon Sep 17 00:00:00 2001 From: Dennis Zollo Date: Tue, 5 May 2026 17:34:27 -0400 Subject: [PATCH 4/9] rtcm3: factor SAFEEPHEM_STALE_THRESHOLD_S macro 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) --- src/rtcm3.c | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/rtcm3.c b/src/rtcm3.c index a0836775d..b6d15db72 100644 --- a/src/rtcm3.c +++ b/src/rtcm3.c @@ -64,6 +64,11 @@ #define P2_59 1.734723475976810E-18 /* 2^-59 */ #define P2_66 1.355252715606880E-20 /* 2^-66 */ +/* 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. */ +#define SAFEEPHEM_STALE_THRESHOLD_S (86400.0*3.0) + /* type definition -----------------------------------------------------------*/ typedef struct { /* multi-signal-message header type */ @@ -815,7 +820,7 @@ static int decode_type1019(rtcm_t *rtcm) eph.toc=gpst2time(eph.week,toc); if (safeephem) { tt=timediff(eph.toe,rtcm->time); - if (fabs(tt)>86400.0*3.0) { + if (fabs(tt)>SAFEEPHEM_STALE_THRESHOLD_S) { trace(2,"rtcm3 1019 -SAFEEPHEMTIME: stale eph prn=%d age=%.1f days\n", prn,-tt/86400.0); } @@ -1148,7 +1153,7 @@ static int decode_type1041(rtcm_t *rtcm) eph.toc=gpst2time(eph.week,toc); if (safeephem) { tt=timediff(eph.toe,rtcm->time); - if (fabs(tt)>86400.0*3.0) { + if (fabs(tt)>SAFEEPHEM_STALE_THRESHOLD_S) { trace(2,"rtcm3 1041 -SAFEEPHEMTIME: stale eph prn=%d age=%.1f days\n", prn,-tt/86400.0); } @@ -1237,7 +1242,7 @@ static int decode_type1044(rtcm_t *rtcm) eph.toc=gpst2time(eph.week,toc); if (safeephem) { tt=timediff(eph.toe,rtcm->time); - if (fabs(tt)>86400.0*3.0) { + if (fabs(tt)>SAFEEPHEM_STALE_THRESHOLD_S) { trace(2,"rtcm3 1044 -SAFEEPHEMTIME: stale eph prn=%d age=%.1f days\n", prn,-tt/86400.0); } @@ -1331,7 +1336,7 @@ static int decode_type1045(rtcm_t *rtcm) eph.toc=gpst2time(eph.week,toc); if (safeephem) { tt=timediff(eph.toe,rtcm->time); - if (fabs(tt)>86400.0*3.0) { + if (fabs(tt)>SAFEEPHEM_STALE_THRESHOLD_S) { trace(2,"rtcm3 1045 -SAFEEPHEMTIME: stale eph prn=%d age=%.1f days\n", prn,-tt/86400.0); } @@ -1428,7 +1433,7 @@ static int decode_type1046(rtcm_t *rtcm) eph.toc=gpst2time(eph.week,toc); if (safeephem) { tt=timediff(eph.toe,rtcm->time); - if (fabs(tt)>86400.0*3.0) { + if (fabs(tt)>SAFEEPHEM_STALE_THRESHOLD_S) { trace(2,"rtcm3 1046 -SAFEEPHEMTIME: stale eph prn=%d age=%.1f days\n", prn,-tt/86400.0); } @@ -1520,7 +1525,7 @@ static int decode_type1042(rtcm_t *rtcm) eph.toc=bdt2gpst(bdt2time(eph.week,toc)); /* bdt -> gpst */ if (safeephem) { tt=timediff(eph.toe,rtcm->time); - if (fabs(tt)>86400.0*3.0) { + if (fabs(tt)>SAFEEPHEM_STALE_THRESHOLD_S) { trace(2,"rtcm3 1042 -SAFEEPHEMTIME: stale eph prn=%d age=%.1f days\n", prn,-tt/86400.0); } From f3cfa940e9b430220a4d27e1eea977083168f4de Mon Sep 17 00:00:00 2001 From: Dennis Zollo Date: Tue, 5 May 2026 18:08:04 -0400 Subject: [PATCH 5/9] test: add -safeephemtime regression tests to convbin make test 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) --- app/consapp/convbin/gcc/makefile | 36 ++++++++++++++++++++- test/data/rcvraw/igs_ephem_2026-05-01.rtcm | Bin 0 -> 4096 bytes 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 test/data/rcvraw/igs_ephem_2026-05-01.rtcm diff --git a/app/consapp/convbin/gcc/makefile b/app/consapp/convbin/gcc/makefile index ee3244265..2716ced64 100644 --- a/app/consapp/convbin/gcc/makefile +++ b/app/consapp/convbin/gcc/makefile @@ -129,7 +129,9 @@ install: clean: rm -f sbp2rinex sbp2rinex.exe *.o *.obs *.nav *.gnav *.hnav *.qnav *.sbs *.stackdump -test : test1 test2 test3 test4 test5 test6 testclean +test : test1 test2 test3 test4 test5 test6 \ + test_safeephem_guard test_safeephem_sbp_anchors test_safeephem_rtcm3_anchors \ + testclean test1: # binary sbp @echo -e "\n\n****** Test1: sbp binary ********\n" @@ -155,5 +157,37 @@ test6: # BDS + GAL + GLO + GPS diff --strip-trailing-cr ./2018-06-15-json.trunc.obs $(shell pwd)$(SEP)$(DATDIR)$(SEP)2018-06-15-json-ref.obs tail -n +5 ./2018-06-15-json.nav > 2018-06-15-json.trunc.nav diff --strip-trailing-cr ./2018-06-15-json.trunc.nav $(shell pwd)$(SEP)$(DATDIR)$(SEP)2018-06-15-json-ref.nav +test_safeephem_guard: + @echo -e "\n\n****** test_safeephem_guard: -safeephemtime without -tr ******\n" + @if ./sbp2rinex -safeephemtime $(DATDIR)$(SEP)2017-05-12-v1.1.26.sbp \ + -o se_guard.obs >/dev/null 2>&1; then \ + echo "FAIL: expected non-zero exit"; exit 1; \ + else \ + echo "OK: rejected as expected"; \ + fi + +test_safeephem_sbp_anchors: + @echo -e "\n\n****** test_safeephem_sbp_anchors: 2017 SBP fixture anchored to 2017 ******\n" + ./sbp2rinex -safeephemtime -tr 2017/05/12 0:0:0 \ + $(DATDIR)$(SEP)2017-05-12-v1.1.26.sbp \ + -o se_sbp.obs -n se_sbp.nav + @if ! grep -qE '^[A-Z][0-9]+ +2017 ' se_sbp.nav; then \ + echo "FAIL: no 2017 epochs in SBP nav file"; exit 1; \ + fi + @if grep -qE '^[A-Z][0-9]+ +202[5-9] ' se_sbp.nav; then \ + echo "FAIL: SBP nav contains current-era epochs (rollover regression)"; exit 1; \ + fi + @echo "OK: SBP nav epochs anchored to 2017" + +test_safeephem_rtcm3_anchors: + @echo -e "\n\n****** test_safeephem_rtcm3_anchors: 2026 IGS RTCM3 fixture ******\n" + ./sbp2rinex -r rtcm3 -safeephemtime -tr 2026/05/01 0:0:0 \ + $(DATDIR)$(SEP)igs_ephem_2026-05-01.rtcm \ + -o se_rtcm.obs -n se_rtcm.nav + @if ! grep -qE '^[A-Z][0-9]+ +2026 ' se_rtcm.nav; then \ + echo "FAIL: no 2026 epochs in RTCM3 nav file"; exit 1; \ + fi + @echo "OK: RTCM3 nav epochs anchored to 2026" + testclean: rm -f *.o *.obs *.nav *.gnav *.hnav *.qnav *.sbs diff --git a/test/data/rcvraw/igs_ephem_2026-05-01.rtcm b/test/data/rcvraw/igs_ephem_2026-05-01.rtcm new file mode 100644 index 0000000000000000000000000000000000000000..054fac5a77fc3bec47c11ec7890086bb947ccf87 GIT binary patch literal 4096 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 W&SJ3ZiUS_xe?}Q93ts$xjsF0o4kYRT literal 0 HcmV?d00001 From 7ae91b45e354aff796ef2015fa62a2e122dde644 Mon Sep 17 00:00:00 2001 From: Dennis Zollo Date: Wed, 6 May 2026 14:56:39 -0400 Subject: [PATCH 6/9] test: tighten rtcm3 anchor assertion to specific C45 epoch 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) --- app/consapp/convbin/gcc/makefile | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/consapp/convbin/gcc/makefile b/app/consapp/convbin/gcc/makefile index 2716ced64..3fc081b15 100644 --- a/app/consapp/convbin/gcc/makefile +++ b/app/consapp/convbin/gcc/makefile @@ -184,10 +184,15 @@ test_safeephem_rtcm3_anchors: ./sbp2rinex -r rtcm3 -safeephemtime -tr 2026/05/01 0:0:0 \ $(DATDIR)$(SEP)igs_ephem_2026-05-01.rtcm \ -o se_rtcm.obs -n se_rtcm.nav - @if ! grep -qE '^[A-Z][0-9]+ +2026 ' se_rtcm.nav; then \ - echo "FAIL: no 2026 epochs in RTCM3 nav file"; exit 1; \ + @# C45's first eph in this fixture is at BDS week 1058, toe 597600s, + @# more than 3.5 days from the wall clock. Legacy code nudges + @# eph.week++ producing "2026 04 25"; safemode preserves on-wire bits + @# producing "2026 04 18". + @if ! grep -q '^C45 2026 04 18 22 00 00' se_rtcm.nav; then \ + echo "FAIL: C45 first eph not at 2026 04 18 22 00 00"; \ + grep '^C45' se_rtcm.nav | head -2; exit 1; \ fi - @echo "OK: RTCM3 nav epochs anchored to 2026" + @echo "OK: C45 first eph at 2026 04 18 22 00 00 (on-wire week preserved)" testclean: rm -f *.o *.obs *.nav *.gnav *.hnav *.qnav *.sbs From 87a98b0a63000e12d39c820ccacbe49a955ab317 Mon Sep 17 00:00:00 2001 From: Dennis Zollo Date: Wed, 6 May 2026 15:43:27 -0400 Subject: [PATCH 7/9] swiftnav: fix BDS eph.toe construction to GPST gtime_t 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) --- src/rcv/swiftnav.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/rcv/swiftnav.c b/src/rcv/swiftnav.c index 9bd191c57..a0bd5996a 100644 --- a/src/rcv/swiftnav.c +++ b/src/rcv/swiftnav.c @@ -759,10 +759,11 @@ static void decode_bdsnav_common(uint8_t *_pBuff, eph_t *_pEph) { _pEph->iode = U1(_pBuff + 146); _pEph->iodc = U2(_pBuff + 147); - /* SBP carries a full 16-bit GPS week; trust it directly. The internal - rtklib BDS convention stores eph.week as BDS-relative. */ + /* SBP carries full GPS week numbers for BDS messages. Internal rtklib + BDS convention: eph.week is BDS-relative; eph.toe/eph.toc are GPST + gtime_t derived from BDT (week, tow) -- matches rtcm3.c MT 1042. */ _pEph->week = uWeekE - BDS_WEEK_TO_GPS_WEEK; - _pEph->toe = gpst2time(_pEph->week, _pEph->toes); + _pEph->toe = bdt2gpst(bdt2time(_pEph->week, _pEph->toes)); _pEph->toc = gpst2time(uWeekC, dToc); } From 2a6bccef67ca74385bee10c5b7854aae7679f3f7 Mon Sep 17 00:00:00 2001 From: Dennis Zollo Date: Wed, 6 May 2026 15:49:14 -0400 Subject: [PATCH 8/9] improve error string per copilot Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- app/consapp/convbin/convbin.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/consapp/convbin/convbin.c b/app/consapp/convbin/convbin.c index 3a463f98c..f4be47d2a 100644 --- a/app/consapp/convbin/convbin.c +++ b/app/consapp/convbin/convbin.c @@ -582,7 +582,7 @@ int main(int argc, char **argv) tok=(used>0)?" -SAFEEPHEMTIME":"-SAFEEPHEMTIME"; toklen=strlen(tok); if (used+toklen+1>sizeof(opt.rcvopt)) { - fprintf(stderr,"-ro string too long to add -SAFEEPHEMTIME\n"); + fprintf(stderr,"receiver option string too long to add -SAFEEPHEMTIME\n"); return -1; } memcpy(opt.rcvopt+used,tok,toklen+1); From b91201501aa72e748085b9cdd31505a908fa29c5 Mon Sep 17 00:00:00 2001 From: Dennis Zollo Date: Thu, 7 May 2026 13:30:14 -0400 Subject: [PATCH 9/9] rtcm3: refactor via trace_safeephem_stale helper 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) --- src/rtcm3.c | 61 +++++++++++++++++------------------------------------ 1 file changed, 19 insertions(+), 42 deletions(-) diff --git a/src/rtcm3.c b/src/rtcm3.c index b6d15db72..33cecd946 100644 --- a/src/rtcm3.c +++ b/src/rtcm3.c @@ -195,6 +195,19 @@ static int adjbdtweek(int week) if (w<1) w=1; /* use 2006/1/1 if time is earlier than 2006/1/1 */ return week+(w-week+4095)/8192*8192; } +/* Trace a SAFEEPHEMTIME warning if the decoded TOE is too far from the + current rtcm reference time. No-op when safemode is off. */ +static void trace_safeephem_stale(int safeephem, gtime_t toe, gtime_t time, + int prn, int msg_id) +{ + double tt; + if (!safeephem) return; + tt=timediff(toe,time); + if (fabs(tt)>SAFEEPHEM_STALE_THRESHOLD_S) { + trace(2,"rtcm3 %d -SAFEEPHEMTIME: stale eph prn=%d age=%.1f days\n", + msg_id,prn,-tt/86400.0); + } +} /* adjust daily rollover of GLONASS time -------------------------------------*/ static void adjday_glot(rtcm_t *rtcm, double tod) { @@ -818,13 +831,7 @@ static int decode_type1019(rtcm_t *rtcm) } eph.toe=gpst2time(eph.week,eph.toes); eph.toc=gpst2time(eph.week,toc); - if (safeephem) { - tt=timediff(eph.toe,rtcm->time); - if (fabs(tt)>SAFEEPHEM_STALE_THRESHOLD_S) { - trace(2,"rtcm3 1019 -SAFEEPHEMTIME: stale eph prn=%d age=%.1f days\n", - prn,-tt/86400.0); - } - } + trace_safeephem_stale(safeephem,eph.toe,rtcm->time,prn,1019); } eph.ttr=rtcm->time; eph.A=sqrtA*sqrtA; @@ -1151,13 +1158,7 @@ static int decode_type1041(rtcm_t *rtcm) } eph.toe=gpst2time(eph.week,eph.toes); eph.toc=gpst2time(eph.week,toc); - if (safeephem) { - tt=timediff(eph.toe,rtcm->time); - if (fabs(tt)>SAFEEPHEM_STALE_THRESHOLD_S) { - trace(2,"rtcm3 1041 -SAFEEPHEMTIME: stale eph prn=%d age=%.1f days\n", - prn,-tt/86400.0); - } - } + trace_safeephem_stale(safeephem,eph.toe,rtcm->time,prn,1041); } eph.ttr=rtcm->time; eph.A=sqrtA*sqrtA; @@ -1240,13 +1241,7 @@ static int decode_type1044(rtcm_t *rtcm) } eph.toe=gpst2time(eph.week,eph.toes); eph.toc=gpst2time(eph.week,toc); - if (safeephem) { - tt=timediff(eph.toe,rtcm->time); - if (fabs(tt)>SAFEEPHEM_STALE_THRESHOLD_S) { - trace(2,"rtcm3 1044 -SAFEEPHEMTIME: stale eph prn=%d age=%.1f days\n", - prn,-tt/86400.0); - } - } + trace_safeephem_stale(safeephem,eph.toe,rtcm->time,prn,1044); } eph.ttr=rtcm->time; eph.A=sqrtA*sqrtA; @@ -1334,13 +1329,7 @@ static int decode_type1045(rtcm_t *rtcm) } eph.toe=gpst2time(eph.week,eph.toes); eph.toc=gpst2time(eph.week,toc); - if (safeephem) { - tt=timediff(eph.toe,rtcm->time); - if (fabs(tt)>SAFEEPHEM_STALE_THRESHOLD_S) { - trace(2,"rtcm3 1045 -SAFEEPHEMTIME: stale eph prn=%d age=%.1f days\n", - prn,-tt/86400.0); - } - } + trace_safeephem_stale(safeephem,eph.toe,rtcm->time,prn,1045); } eph.ttr=rtcm->time; eph.A=sqrtA*sqrtA; @@ -1431,13 +1420,7 @@ static int decode_type1046(rtcm_t *rtcm) } eph.toe=gpst2time(eph.week,eph.toes); eph.toc=gpst2time(eph.week,toc); - if (safeephem) { - tt=timediff(eph.toe,rtcm->time); - if (fabs(tt)>SAFEEPHEM_STALE_THRESHOLD_S) { - trace(2,"rtcm3 1046 -SAFEEPHEMTIME: stale eph prn=%d age=%.1f days\n", - prn,-tt/86400.0); - } - } + trace_safeephem_stale(safeephem,eph.toe,rtcm->time,prn,1046); } eph.ttr=rtcm->time; eph.A=sqrtA*sqrtA; @@ -1523,13 +1506,7 @@ static int decode_type1042(rtcm_t *rtcm) } eph.toe=bdt2gpst(bdt2time(eph.week,eph.toes)); /* bdt -> gpst */ eph.toc=bdt2gpst(bdt2time(eph.week,toc)); /* bdt -> gpst */ - if (safeephem) { - tt=timediff(eph.toe,rtcm->time); - if (fabs(tt)>SAFEEPHEM_STALE_THRESHOLD_S) { - trace(2,"rtcm3 1042 -SAFEEPHEMTIME: stale eph prn=%d age=%.1f days\n", - prn,-tt/86400.0); - } - } + trace_safeephem_stale(safeephem,eph.toe,rtcm->time,prn,1042); } eph.ttr=rtcm->time; eph.A=sqrtA*sqrtA;