Coverage Report

Created: 2026-02-26 06:20

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/ntp-dev/libntp/ntp_calgps.c
Line
Count
Source
1
/*
2
 * ntp_calgps.c - calendar for GPS/GNSS based clocks
3
 *
4
 * Written by Juergen Perlinger (perlinger@ntp.org) for the NTP project.
5
 * The contents of 'html/copyright.html' apply.
6
 *
7
 * --------------------------------------------------------------------
8
 *
9
 * This module implements stuff often used with GPS/GNSS receivers
10
 */
11
12
#include <config.h>
13
#include <sys/types.h>
14
15
#include "ntp_types.h"
16
#include "ntp_calendar.h"
17
#include "ntp_calgps.h"
18
#include "ntp_stdlib.h"
19
#include "ntp_unixtime.h"
20
21
#include "ntp_fp.h"
22
#include "ntpd.h"
23
#include "vint64ops.h"
24
25
/* ====================================================================
26
 * misc. helpers -- might go elsewhere sometime?
27
 * ====================================================================
28
 */
29
30
l_fp
31
ntpfp_with_fudge(
32
  l_fp  lfp,
33
  double  ofs
34
  )
35
0
{
36
0
  l_fp  fpo;
37
  /* calculate 'lfp - ofs' as '(l_fp)(-ofs) + lfp': negating a
38
   * double is cheap, as it only flips one bit...
39
   */
40
0
  ofs = -ofs;
41
0
  DTOLFP(ofs, &fpo);
42
0
  L_ADD(&fpo, &lfp);
43
0
  return fpo;
44
0
}
45
46
47
/* ====================================================================
48
 * GPS calendar functions
49
 * ====================================================================
50
 */
51
52
/* --------------------------------------------------------------------
53
 * normalization functions for day/time and week/time representations.
54
 * Since we only use moderate offsets (leap second corrections and
55
 * alike) it does not really pay off to do a floor-corrected division
56
 * here.  We use compare/decrement/increment loops instead.
57
 * --------------------------------------------------------------------
58
 */
59
static void
60
_norm_ntp_datum(
61
  TNtpDatum * datum
62
  )
63
0
{
64
0
  static const int32_t limit = SECSPERDAY;
65
66
0
  if (datum->secs >= limit) {
67
0
    do
68
0
      ++datum->days;
69
0
    while ((datum->secs -= limit) >= limit);
70
0
  } else if (datum->secs < 0) {
71
0
    do
72
0
      --datum->days;
73
0
    while ((datum->secs += limit) < 0);
74
0
  }
75
0
}
76
77
static void
78
_norm_gps_datum(
79
  TGpsDatum * datum
80
  )
81
0
{
82
0
  static const int32_t limit = 7 * SECSPERDAY;
83
84
0
  if (datum->wsecs >= limit) {
85
0
    do
86
0
      ++datum->weeks;
87
0
    while ((datum->wsecs -= limit) >= limit);
88
0
  } else if (datum->wsecs < 0) {
89
0
    do
90
0
      --datum->weeks;
91
0
    while ((datum->wsecs += limit) < 0);
92
0
  }
93
0
}
94
95
/* --------------------------------------------------------------------
96
 * Add an offset to a day/time and week/time representation.
97
 *
98
 * !!Attention!! the offset should be small, compared to the time period
99
 * (either a day or a week).
100
 * --------------------------------------------------------------------
101
 */
102
void
103
gpsntp_add_offset(
104
  TNtpDatum * datum,
105
  l_fp    offset
106
  )
107
0
{
108
  /* fraction can be added easily */
109
0
  datum->frac += offset.l_uf;
110
0
  datum->secs += (datum->frac < offset.l_uf);
111
112
  /* avoid integer overflow on the seconds */
113
0
  if (offset.l_ui >= INT32_MAX)
114
0
    datum->secs -= (int32_t)~offset.l_ui + 1;
115
0
  else
116
0
    datum->secs += (int32_t)offset.l_ui;
117
0
  _norm_ntp_datum(datum);
118
0
}
119
120
void
121
gpscal_add_offset(
122
  TGpsDatum * datum,
123
  l_fp    offset
124
  )
125
0
{
126
  /* fraction can be added easily */
127
0
  datum->frac  += offset.l_uf;
128
0
  datum->wsecs += (datum->frac < offset.l_uf);
129
130
131
  /* avoid integer overflow on the seconds */
132
0
  if (offset.l_ui >= INT32_MAX)
133
0
    datum->wsecs -= (int32_t)~offset.l_ui + 1;
134
0
  else
135
0
    datum->wsecs += (int32_t)offset.l_ui;
136
0
  _norm_gps_datum(datum);
137
0
}
138
139
/* -------------------------------------------------------------------
140
 *  API functions civil calendar and NTP datum
141
 * -------------------------------------------------------------------
142
 */
143
144
static TNtpDatum
145
_gpsntp_fix_gps_era(
146
  TcNtpDatum * in
147
  )
148
0
{
149
  /* force result in basedate era
150
   *
151
   * When calculating this directly in days, we have to execute a
152
   * real modulus calculation, since we're obviously not doing a
153
   * modulus by a power of 2. Executing this as true floor mod
154
   * needs some care and is done under explicit usage of one's
155
   * complement and masking to get mostly branchless code.
156
   */
157
0
  static uint32_t const clen = 7*1024;
158
159
0
  uint32_t  base, days, sign;
160
0
  TNtpDatum out = *in;
161
162
  /* Get base in NTP day scale. No overflows here. */
163
0
  base = (basedate_get_gpsweek() + GPSNTP_WSHIFT) * 7
164
0
       - GPSNTP_DSHIFT;
165
0
  days = out.days;
166
167
0
  sign = (uint32_t)-(days < base);
168
0
  days = sign ^ (days - base);
169
0
  days %= clen;
170
0
  days = base + (sign & clen) + (sign ^ days);
171
172
0
  out.days = days;
173
0
  return out;
174
0
}
175
176
TNtpDatum
177
gpsntp_fix_gps_era(
178
  TcNtpDatum * in
179
  )
180
0
{
181
0
  TNtpDatum out = *in;
182
0
  _norm_ntp_datum(&out);
183
0
  return _gpsntp_fix_gps_era(&out);
184
0
}
185
186
/* ----------------------------------------------------------------- */
187
static TNtpDatum
188
_gpsntp_from_daytime(
189
  TcCivilDate * jd,
190
  l_fp    fofs,
191
  TcNtpDatum *  pivot,
192
  int   warp
193
  )
194
0
{
195
0
  static const int32_t shift = SECSPERDAY / 2;
196
197
0
  TNtpDatum retv;
198
199
  /* set result based on pivot -- ops order is important here */
200
0
  ZERO(retv);
201
0
  retv.secs = ntpcal_date_to_daysec(jd);
202
0
  gpsntp_add_offset(&retv, fofs); /* result is normalized */
203
0
  retv.days = pivot->days;
204
205
  /* Manual periodic extension without division: */
206
0
  if (pivot->secs < shift) {
207
0
    int32_t lim = pivot->secs + shift;
208
0
    retv.days -= (retv.secs > lim ||
209
0
            (retv.secs == lim && retv.frac >= pivot->frac));
210
0
  } else {
211
0
    int32_t lim = pivot->secs - shift;
212
0
    retv.days += (retv.secs < lim ||
213
0
            (retv.secs == lim && retv.frac < pivot->frac));
214
0
  }
215
0
  return warp ? _gpsntp_fix_gps_era(&retv) : retv;
216
0
}
217
218
/* -----------------------------------------------------------------
219
 * Given the time-of-day part of a civil datum and an additional
220
 * (fractional) offset, calculate a full time stamp around a given pivot
221
 * time so that the difference between the pivot and the resulting time
222
 * stamp is less or equal to 12 hours absolute.
223
 */
224
TNtpDatum
225
gpsntp_from_daytime2_ex(
226
  TcCivilDate * jd,
227
  l_fp    fofs,
228
  TcNtpDatum *  pivot,
229
  int/*BOOL*/ warp
230
  )
231
0
{
232
0
  TNtpDatum dpiv = *pivot;
233
0
  _norm_ntp_datum(&dpiv);
234
0
  return _gpsntp_from_daytime(jd, fofs, &dpiv, warp);
235
0
}
236
237
/* -----------------------------------------------------------------
238
 * This works similar to 'gpsntp_from_daytime1()' and actually even uses
239
 * it, but the pivot is calculated from the pivot given as 'l_fp' in NTP
240
 * time scale. This is in turn expanded around the current system time,
241
 * and the resulting absolute pivot is then used to calculate the full
242
 * NTP time stamp.
243
 */
244
TNtpDatum
245
gpsntp_from_daytime1_ex(
246
  TcCivilDate * jd,
247
  l_fp    fofs,
248
  l_fp    pivot,
249
  int/*BOOL*/ warp
250
  )
251
0
{
252
0
  vint64    pvi64;
253
0
  TNtpDatum dpiv;
254
0
  ntpcal_split  split;
255
256
0
  pvi64 = ntpcal_ntp_to_ntp(pivot.l_ui, NULL);
257
0
  split = ntpcal_daysplit(&pvi64);
258
0
  dpiv.days = split.hi;
259
0
  dpiv.secs = split.lo;
260
0
  dpiv.frac = pivot.l_uf;
261
0
  return _gpsntp_from_daytime(jd, fofs, &dpiv, warp);
262
0
}
263
264
/* -----------------------------------------------------------------
265
 * Given a calendar date, zap it into a GPS time format and then convert
266
 * that one into the NTP time scale.
267
 */
268
TNtpDatum
269
gpsntp_from_calendar_ex(
270
  TcCivilDate * jd,
271
  l_fp    fofs,
272
  int/*BOOL*/ warp
273
  )
274
0
{
275
0
  TGpsDatum gps;
276
0
  gps = gpscal_from_calendar_ex(jd, fofs, warp);
277
0
  return gpsntp_from_gpscal_ex(&gps, FALSE);
278
0
}
279
280
/* -----------------------------------------------------------------
281
 * create a civil calendar datum from a NTP date representation
282
 */
283
void
284
gpsntp_to_calendar(
285
  TCivilDate * cd,
286
  TcNtpDatum * nd
287
  )
288
0
{
289
0
  memset(cd, 0, sizeof(*cd));
290
0
  ntpcal_rd_to_date(
291
0
    cd,
292
0
    nd->days + DAY_NTP_STARTS + ntpcal_daysec_to_date(
293
0
      cd, nd->secs));
294
0
}
295
296
/* -----------------------------------------------------------------
297
 * get day/tod representation from week/tow datum
298
 */
299
TNtpDatum
300
gpsntp_from_gpscal_ex(
301
  TcGpsDatum *  gd,
302
      int/*BOOL*/ warp
303
  )
304
0
{
305
0
  TNtpDatum retv;
306
0
  vint64    ts64;
307
0
  ntpcal_split  split;
308
0
  TGpsDatum date = *gd;
309
310
0
  if (warp) {
311
0
    uint32_t base = basedate_get_gpsweek() + GPSNTP_WSHIFT;
312
0
    _norm_gps_datum(&date);
313
0
    date.weeks = ((date.weeks - base) & 1023u) + base;
314
0
  }
315
316
0
  ts64  = ntpcal_weekjoin(date.weeks, date.wsecs);
317
0
  ts64  = subv64u32(&ts64, (GPSNTP_DSHIFT * SECSPERDAY));
318
0
  split = ntpcal_daysplit(&ts64);
319
320
0
  retv.frac = gd->frac;
321
0
  retv.secs = split.lo;
322
0
  retv.days = split.hi;
323
0
  return retv;
324
0
}
325
326
/* -----------------------------------------------------------------
327
 * get LFP from ntp datum
328
 */
329
l_fp
330
ntpfp_from_ntpdatum(
331
  TcNtpDatum *  nd
332
  )
333
0
{
334
0
  l_fp retv;
335
336
0
  retv.l_uf = nd->frac;
337
0
  retv.l_ui = nd->days * (uint32_t)SECSPERDAY
338
0
            + nd->secs;
339
0
  return retv;
340
0
}
341
342
/* -------------------------------------------------------------------
343
 *  API functions GPS week calendar
344
 *
345
 * Here we use a calendar base of 1899-12-31, so the NTP epoch has
346
 * { 0, 86400.0 } in this representation.
347
 * -------------------------------------------------------------------
348
 */
349
350
static TGpsDatum
351
_gpscal_fix_gps_era(
352
  TcGpsDatum * in
353
  )
354
0
{
355
  /* force result in basedate era
356
   *
357
   * This is based on calculating the modulus to a power of two,
358
   * so signed integer overflow does not affect the result. Which
359
   * in turn makes for a very compact calculation...
360
   */
361
0
  uint32_t  base, week;
362
0
  TGpsDatum out = *in;
363
364
0
  week = out.weeks;
365
0
  base = basedate_get_gpsweek() + GPSNTP_WSHIFT;
366
0
  week = base + ((week - base) & (GPSWEEKS - 1));
367
0
  out.weeks = week;
368
0
  return out;
369
0
}
370
371
TGpsDatum
372
gpscal_fix_gps_era(
373
  TcGpsDatum * in
374
  )
375
0
{
376
0
  TGpsDatum out = *in;
377
0
  _norm_gps_datum(&out);
378
0
  return _gpscal_fix_gps_era(&out);
379
0
}
380
381
/* -----------------------------------------------------------------
382
 * Given a calendar date, zap it into a GPS time format and the do a
383
 * proper era mapping in the GPS time scale, based on the GPS base date,
384
 * if so requested.
385
 *
386
 * This function also augments the century if just a 2-digit year
387
 * (0..99) is provided on input.
388
 *
389
 * This is a fail-safe against GPS receivers with an unknown starting
390
 * point for their internal calendar calculation and therefore
391
 * unpredictable (but reproducible!) rollover behavior. While there
392
 * *are* receivers that create a full date in the proper way, many
393
 * others just don't.  The overall damage is minimized by simply not
394
 * trusting the era mapping of the receiver and doing the era assignment
395
 * with a configurable base date *inside* ntpd.
396
 */
397
TGpsDatum
398
gpscal_from_calendar_ex(
399
  TcCivilDate * jd,
400
  l_fp    fofs,
401
  int/*BOOL*/ warp
402
  )
403
0
{
404
  /*  (-DAY_GPS_STARTS) (mod 7*1024) -- complement of cycle shift */
405
0
  static const uint32_t s_compl_shift =
406
0
      (7 * 1024) - DAY_GPS_STARTS % (7 * 1024);
407
408
0
  TGpsDatum gps;
409
0
  TCivilDate  cal;
410
0
  int32_t   days, week;
411
412
  /* if needed, convert from 2-digit year to full year
413
   * !!NOTE!! works only between 1980 and 2079!
414
   */
415
0
  cal = *jd;
416
0
  if (cal.year < 80)
417
0
    cal.year += 2000;
418
0
  else if (cal.year < 100)
419
0
    cal.year += 1900;
420
421
  /* get RDN from date, possibly adjusting the century */
422
0
again:  if (cal.month && cal.monthday) { /* use Y/M/D civil date */
423
0
    days = ntpcal_date_to_rd(&cal);
424
0
  } else {       /* using Y/DoY date */
425
0
    days = ntpcal_year_to_ystart(cal.year)
426
0
         + (int32_t)cal.yearday
427
0
         - 1; /* both RDN and yearday start with '1'. */
428
0
  }
429
430
  /* Rebase to days after the GPS epoch. 'days' is positive here,
431
   * but it might be less than the GPS epoch start. Depending on
432
   * the input, we have to do different things to get the desired
433
   * result. (Since we want to remap the era anyway, we only have
434
   * to retain congruential identities....)
435
   */
436
437
0
  if (days >= DAY_GPS_STARTS) {
438
    /* simply shift to days since GPS epoch */
439
0
    days -= DAY_GPS_STARTS;
440
0
  } else if (jd->year < 100) {
441
    /* Two-digit year on input: add another century and
442
     * retry.  This can happen only if the century expansion
443
     * yielded a date between 1980-01-01 and 1980-01-05,
444
     * both inclusive. We have at most one retry here.
445
     */
446
0
    cal.year += 100;
447
0
    goto again;
448
0
  } else {
449
    /* A very bad date before the GPS epoch. There's not
450
     * much we can do, except to add the complement of
451
     * DAY_GPS_STARTS % (7 * 1024) here, that is, use a
452
     * congruential identity: Add the complement instead of
453
     * subtracting the value gives a value with the same
454
     * modulus. But of course, now we MUST to go through a
455
     * cycle fix... because the date was obviously wrong!
456
     */
457
0
    warp  = TRUE;
458
0
    days += s_compl_shift;
459
0
  }
460
461
  /* Splitting to weeks is simple now: */
462
0
  week  = days / 7;
463
0
  days -= week * 7;
464
465
  /* re-base on start of NTP with weeks mapped to 1024 weeks
466
   * starting with the GPS base day set in the calendar.
467
   */
468
0
  gps.weeks = week + GPSNTP_WSHIFT;
469
0
  gps.wsecs = days * SECSPERDAY + ntpcal_date_to_daysec(&cal);
470
0
  gps.frac  = 0;
471
0
  gpscal_add_offset(&gps, fofs);
472
0
  return warp ? _gpscal_fix_gps_era(&gps) : gps;
473
0
}
474
475
/* -----------------------------------------------------------------
476
 * get civil date from week/tow representation
477
 */
478
void
479
gpscal_to_calendar(
480
  TCivilDate * cd,
481
  TcGpsDatum * wd
482
  )
483
0
{
484
0
  TNtpDatum nd;
485
486
0
  memset(cd, 0, sizeof(*cd));
487
0
  nd = gpsntp_from_gpscal_ex(wd, FALSE);
488
0
  gpsntp_to_calendar(cd, &nd);
489
0
}
490
491
/* -----------------------------------------------------------------
492
 * Given the week and seconds in week, as well as the fraction/offset
493
 * (which should/could include the leap seconds offset), unfold the
494
 * weeks (which are assumed to have just 10 bits) into expanded weeks
495
 * based on the GPS base date derived from the build date (default) or
496
 * set by the configuration.
497
 *
498
 * !NOTE! This function takes RAW GPS weeks, aligned to the GPS start
499
 * (1980-01-06) on input. The output weeks will be aligned to NTPD's
500
 * week calendar start (1899-12-31)!
501
 */
502
TGpsDatum
503
gpscal_from_gpsweek(
504
  uint16_t  week,
505
  int32_t   secs,
506
  l_fp    fofs
507
  )
508
0
{
509
0
  TGpsDatum retv;
510
511
0
  retv.frac  = 0;
512
0
  retv.wsecs = secs;
513
0
  retv.weeks = week + GPSNTP_WSHIFT;
514
0
  gpscal_add_offset(&retv, fofs);
515
0
  return _gpscal_fix_gps_era(&retv);
516
0
}
517
518
/* -----------------------------------------------------------------
519
 * internal work horse for time-of-week expansion
520
 */
521
static TGpsDatum
522
_gpscal_from_weektime(
523
  int32_t   wsecs,
524
  l_fp      fofs,
525
  TcGpsDatum *  pivot
526
  )
527
0
{
528
0
  static const int32_t shift = SECSPERWEEK / 2;
529
530
0
  TGpsDatum retv;
531
532
  /* set result based on pivot -- ops order is important here */
533
0
  ZERO(retv);
534
0
  retv.wsecs = wsecs;
535
0
  gpscal_add_offset(&retv, fofs); /* result is normalized */
536
0
  retv.weeks = pivot->weeks;
537
538
  /* Manual periodic extension without division: */
539
0
  if (pivot->wsecs < shift) {
540
0
    int32_t lim = pivot->wsecs + shift;
541
0
    retv.weeks -= (retv.wsecs > lim ||
542
0
             (retv.wsecs == lim && retv.frac >= pivot->frac));
543
0
  } else {
544
0
    int32_t lim = pivot->wsecs - shift;
545
0
    retv.weeks += (retv.wsecs < lim ||
546
0
             (retv.wsecs == lim && retv.frac < pivot->frac));
547
0
  }
548
0
  return _gpscal_fix_gps_era(&retv);
549
0
}
550
551
/* -----------------------------------------------------------------
552
 * expand a time-of-week around a pivot given as week datum
553
 */
554
TGpsDatum
555
gpscal_from_weektime2(
556
  int32_t   wsecs,
557
  l_fp      fofs,
558
  TcGpsDatum *  pivot
559
  )
560
0
{
561
0
  TGpsDatum wpiv = * pivot;
562
0
  _norm_gps_datum(&wpiv);
563
0
  return _gpscal_from_weektime(wsecs, fofs, &wpiv);
564
0
}
565
566
/* -----------------------------------------------------------------
567
 * epand a time-of-week around an pivot given as LFP, which in turn
568
 * is expanded around the current system time and then converted
569
 * into a week datum.
570
 */
571
TGpsDatum
572
gpscal_from_weektime1(
573
  int32_t wsecs,
574
  l_fp    fofs,
575
  l_fp    pivot
576
  )
577
0
{
578
0
  vint64    pvi64;
579
0
  TGpsDatum wpiv;
580
0
  ntpcal_split  split;
581
582
  /* get 64-bit pivot in NTP epoch */
583
0
  pvi64 = ntpcal_ntp_to_ntp(pivot.l_ui, NULL);
584
585
  /* convert to weeks since 1899-12-31 and seconds in week */
586
0
  pvi64 = addv64u32(&pvi64, (GPSNTP_DSHIFT * SECSPERDAY));
587
0
  split = ntpcal_weeksplit(&pvi64);
588
589
0
  wpiv.weeks = split.hi;
590
0
  wpiv.wsecs = split.lo;
591
0
  wpiv.frac  = pivot.l_uf;
592
0
  return _gpscal_from_weektime(wsecs, fofs, &wpiv);
593
0
}
594
595
/* -----------------------------------------------------------------
596
 * get week/tow representation from day/tod datum
597
 */
598
TGpsDatum
599
gpscal_from_gpsntp(
600
  TcNtpDatum *  gd
601
  )
602
0
{
603
0
  TGpsDatum retv;
604
0
  vint64    ts64;
605
0
  ntpcal_split  split;
606
607
0
  ts64  = ntpcal_dayjoin(gd->days, gd->secs);
608
0
  ts64  = addv64u32(&ts64, (GPSNTP_DSHIFT * SECSPERDAY));
609
0
  split = ntpcal_weeksplit(&ts64);
610
611
0
  retv.frac  = gd->frac;
612
0
  retv.wsecs = split.lo;
613
0
  retv.weeks = split.hi;
614
0
  return retv;
615
0
}
616
617
/* -----------------------------------------------------------------
618
 * convert week/tow to LFP stamp
619
 */
620
l_fp
621
ntpfp_from_gpsdatum(
622
  TcGpsDatum *  gd
623
  )
624
0
{
625
0
  l_fp retv;
626
627
0
  retv.l_uf = gd->frac;
628
0
  retv.l_ui = gd->weeks * (uint32_t)SECSPERWEEK
629
0
            + (uint32_t)gd->wsecs
630
0
            - (uint32_t)SECSPERDAY * GPSNTP_DSHIFT;
631
0
  return retv;
632
0
}
633
634
/* -*-EOF-*- */