[chrony-dev] [PATCH] Add support for the UT1 time scale

[ Thread Index | Date Index | More chrony.tuxfamily.org/chrony-dev Archives ]


In modern terrestrial astronomy, automation of experiments is
widespread, and an astronomical telescope and radiation receiver often
use dozens of embedded specialized computers that control the movement
of optical elements or the collection of information. Some of the
software here naturally needs the UT1 time scale, and is forced to get
it using system time (UTC) and correction (dUT1), which varies over
time and is usually published on the Internet. Note that at the moment
of the leap second, additional technical complexity is added to account
for it. Since the embedded computer is specialized (the main software
on it is the software that controls, for example, the movement of the
telescope), there is a natural desire to try to keep the operating
system in the desired UT1 time scale. And to use the existing NTP
infrastructure to synchronize and maintain accurate time, but on this
time scale.

NTP servers running in the UT1 time scale are known (https://
www.nist.gov/pml/time-and-frequency-division/time-services/
ut1-ntp-time-dissemination). However, to access them, one needs to
register in NIST and obtain an account, their software is unknown and
is not open source, i.e. use is difficult, and deployment of new UT1
NTP servers is impossible. I suggest filling this gap and trying to
implement UT1 support in the open source project - chrony.

NTP time sources operating in the UT1 time scale can be used along with
UTC sources. There will be a configuration option to indicate that the
source is running in the UT1 timescale.

If the NTP server is running in the UT1 time scale, then the UT1 time
will be calculated using UTC time and the dUT1 correction. The value of
dUT1 can be obtained in these ways:

- take a constant value from the configuration

- calculate the value based on data from the local Bulletin A CSV file
(https://datacenter.iers.org/productMetadata.php?id=6)
---
 cmdparse.c           |   3 +
 conf.c               | 168 +++++++++++++++++++++++++++++++++++++++++++
 conf.h               |  13 ++++
 doc/chrony.conf.adoc |  19 +++++
 ntp_core.c           |   8 +++
 reference.c          |   3 +
 srcparams.h          |   1 +
 7 files changed, 215 insertions(+)

diff --git a/cmdparse.c b/cmdparse.c
index 0a80fc0..952247c 100644
--- a/cmdparse.c
+++ b/cmdparse.c
@@ -77,6 +77,7 @@ CPS_ParseNTPSourceAdd(char *line, CPS_NTP_Source *src)
   src->params.min_delay = 0.0;
   src->params.asymmetry = SRC_DEFAULT_ASYMMETRY;
   src->params.offset = 0.0;
+  src->params.ut1 = 0;
 
   hostname = line;
   line = CPS_SplitWord(line);
@@ -182,6 +183,8 @@ CPS_ParseNTPSourceAdd(char *line, CPS_NTP_Source *src)
     } else if (!strcasecmp(cmd, "presend")) {
       if (sscanf(line, "%d%n", &src->params.presend_minpoll, &n) != 1)
         return 0;
+    } else if (!strcasecmp(cmd, "ut1")) {
+      src->params.ut1 = 1;
     } else if (!strcasecmp(cmd, "version")) {
       if (sscanf(line, "%d%n", &src->params.version, &n) != 1)
         return 0;
diff --git a/conf.c b/conf.c
index 6eae11c..8947438 100644
--- a/conf.c
+++ b/conf.c
@@ -49,6 +49,7 @@
 #define MAX_LINE_LENGTH 2048
 #define MAX_CONF_DIRS 10
 #define MAX_INCLUDE_LEVEL 10
+#define BULLETIN_A_CSV_SEPARATOR ";"
 
 /* ================================================== */
 /* Forward prototypes */
@@ -85,6 +86,7 @@ static void parse_smoothtime(char *);
 static void parse_source(char *line, char *type, int fatal);
 static void parse_sourcedir(char *);
 static void parse_tempcomp(char *);
+static void parse_ut1(char *);
 
 /* ================================================== */
 /* Configuration variables */
@@ -321,6 +323,21 @@ typedef struct {
 /* Array of NTP_Broadcast_Destination */
 static ARR_Instance broadcasts;
 
+/* Flag set if UT1 mode is enabled */
+static int ut1 = 0;
+
+/* How to obtain dUT1 constant */
+static CNF_UT1_Source ut1_source;
+
+/* dUT1 constant */
+static double dut1 = 0.0; /* in seconds */
+
+/* dUT1 change per second for linear interpolation */
+static double ddut1 = 0.0; /* in seconds */
+
+/* Path to IERS Bulletin A CSV file which stores dUT1 values */
+static char *bulletin_a_path = NULL;
+
 /* ================================================== */
 
 /* The line number in the configuration file being processed */
@@ -490,6 +507,7 @@ CNF_Finalise(void)
   Free(tempcomp_point_file);
   Free(nts_dump_dir);
   Free(nts_ntp_server);
+  Free(bulletin_a_path);
 }
 
 /* ================================================== */
@@ -739,6 +757,8 @@ CNF_ParseLine(const char *filename, int number, char *line)
     parse_tempcomp(p);
   } else if (!strcasecmp(command, "user")) {
     parse_string(p, &user);
+  } else if (!strcasecmp(command, "ut1")) {
+    parse_ut1(p);
   } else if (!strcasecmp(command, "commandkey") ||
              !strcasecmp(command, "generatecommandkey") ||
              !strcasecmp(command, "linux_freq_scale") ||
@@ -1518,6 +1538,42 @@ parse_hwtimestamp(char *line)
 
 /* ================================================== */
 
+static void
+set_dut1_adjust_timeout()
+{
+  struct timespec when;
+
+  LCL_ReadRawTime(&when);
+  // Set to the next midnight
+  when.tv_sec = (when.tv_sec / (24 * 3600) + 1) * (24 * 3600);
+  when.tv_nsec = 0;
+  SCH_AddTimeout(&when, CNF_SetUT1FromBulletinA, NULL);
+}
+
+/* ================================================== */
+
+static void
+parse_ut1(char *line)
+{
+  char *param = CPS_SplitWord(line);
+
+  if (!strcasecmp(line, "const")) {
+    ut1_source = CNF_UT1_CONSTANT;
+    if (sscanf(param, "%lf", &dut1) != 1)
+      command_parse_error();
+  } else if (!strcasecmp(line, "bulletina")) {
+    ut1_source = CNF_UT1_BULLETINA;
+    bulletin_a_path = Strdup(param);
+  } else {
+    other_parse_error("Invalid ut1 option");
+    return;
+  }
+
+  ut1 = 1;
+}
+
+/* ================================================== */
+
 static const char *
 get_basename(const char *path)
 {
@@ -2662,3 +2718,115 @@ CNF_GetNoCertTimeCheck(void)
 {
   return no_cert_time_check;
 }
+
+/* ================================================== */
+
+int
+CNF_GetUT1(void)
+{
+  return ut1;
+}
+
+/* ================================================== */
+
+CNF_UT1_Source
+CNF_GetUT1Source(void)
+{
+  return ut1_source;
+}
+
+/* ================================================== */
+
+double
+CNF_GetUT1Offset(void)
+{
+  struct timespec now;
+  time_t last_midnight_sec;
+  double offset;
+
+  if (ut1_source == CNF_UT1_BULLETINA)
+    CNF_SetUT1FromBulletinA(NULL);
+  LCL_ReadRawTime(&now);
+  last_midnight_sec = now.tv_sec / (24 * 3600) * (24 * 3600);
+  offset = dut1 + (double)(now.tv_sec - last_midnight_sec) * ddut1;
+  LOG(LOGS_INFO, "Current dUT1: %g", offset);
+  return offset;
+}
+
+/* ================================================== */
+
+void
+CNF_SetUT1FromBulletinA(void *arg)
+{
+  FILE *in;
+  char line[256];
+  int i, mjd_index, dut1_index, read_dut1, need_ddut1, got_ddut1;
+  time_t mjd, mjd_today;
+  char *token, *token_end, *endptr;
+  struct timespec now;
+
+  in = UTI_OpenFile(NULL, bulletin_a_path, NULL, 'R', 0);
+  if (fgets(line, sizeof(line), in) == NULL)
+    LOG_FATAL("Error reading Bulletin A");
+  mjd_index = dut1_index = -1;
+  token = strtok(line, BULLETIN_A_CSV_SEPARATOR);
+  for (i = 0; token != NULL && (mjd_index == -1 || dut1_index == -1);
+       i++, token = strtok(NULL, BULLETIN_A_CSV_SEPARATOR)) {
+    if (!strcmp(token, "MJD"))
+      mjd_index = i;
+    else if (!strcmp(token, "UT1-UTC"))
+      dut1_index = i;
+  }
+
+  LCL_ReadRawTime(&now);
+  mjd = INT_MIN;
+  mjd_today = now.tv_sec / 86400 + 40587;
+  need_ddut1 = 0;
+  got_ddut1 = 0;
+  while (fgets(line, sizeof(line), in)) {
+    read_dut1 = 0;
+    token = line;
+    for (i = 0; i <= mjd_index || i <= dut1_index; i++) {
+      token_end = strchr(token, BULLETIN_A_CSV_SEPARATOR[0]);
+      if (token_end != NULL || (token_end = strchr(token, '\n')) != NULL)
+        *token_end = '\0';
+      if (i == mjd_index)
+        mjd = strtol(token, NULL, 10);
+      else if (i == dut1_index) {
+        if (!need_ddut1)
+          dut1 = strtod(token, &endptr);
+        else {
+          ddut1 = (strtod(token, &endptr) - dut1) / 86400;
+          need_ddut1 = 0;
+          got_ddut1 = 1;
+        }
+        read_dut1 = *endptr == '\0';
+      }
+      if (token_end == NULL)
+        break;
+      token = token_end + 1;
+    }
+    if (mjd >= mjd_today) {
+      if (!read_dut1)
+        LOG_FATAL("Invalid Bulletin A");
+      if (!got_ddut1 && mjd > mjd_today)
+        LOG(LOGS_WARN, "Bulletin A is too new, using the earliest available date");
+      if (!need_ddut1) {
+        if (got_ddut1)
+          break;
+        need_ddut1 = 1;
+      }
+    }
+  }
+
+  if (mjd == INT_MIN)
+    LOG_FATAL("Invalid Bulletin A");
+  LOG(LOGS_INFO, "Got dUT1 = %g from Bulletin A", dut1);
+  if (ddut1)
+    LOG(LOGS_INFO, "Got ddUT1 = %g from Bulletin A", ddut1);
+  if (mjd < mjd_today)
+    LOG(LOGS_WARN, "Bulletin A is too old, using the latest available date");
+  fclose(in);
+
+  set_dut1_adjust_timeout();
+}
diff --git a/conf.h b/conf.h
index 4c0a787..d398fa1 100644
--- a/conf.h
+++ b/conf.h
@@ -29,7 +29,9 @@
 #define GOT_CONF_H
 
 #include "addressing.h"
+#include "local.h"
 #include "reference.h"
+#include "sched.h"
 #include "sources.h"
 
 extern void CNF_Initialise(int restarted, int client_only);
@@ -155,6 +157,12 @@ typedef struct {
   double rx_comp;
 } CNF_HwTsInterface;
 
+typedef enum {
+  CNF_UT1_CONSTANT,
+  CNF_UT1_BULLETINA,
+  CNF_UT1_GPSD,
+} CNF_UT1_Source;
+
 extern int CNF_GetHwTsInterface(unsigned int index, CNF_HwTsInterface **iface);
 extern double CNF_GetHwTsTimeout(void);
 
@@ -174,4 +182,9 @@ extern int CNF_GetNtsTrustedCertsPaths(const char ***paths, uint32_t **ids);
 extern int CNF_GetNoSystemCert(void);
 extern int CNF_GetNoCertTimeCheck(void);
 
+extern int CNF_GetUT1(void);
+extern CNF_UT1_Source CNF_GetUT1Source(void);
+extern double CNF_GetUT1Offset(void);
+extern void CNF_SetUT1FromBulletinA(void *arg);
+
 #endif /* GOT_CONF_H */
diff --git a/doc/chrony.conf.adoc b/doc/chrony.conf.adoc
index bd296bc..179ca33 100644
--- a/doc/chrony.conf.adoc
+++ b/doc/chrony.conf.adoc
@@ -348,6 +348,8 @@ field should be enabled only for servers known to be running *chronyd* version
 These options force *chronyd* to use only IPv4 or IPv6 addresses respectively
 for this source. They do not override the *-4* or *-6* option on the *chronyd*
 command line.
+*ut1*:::
+This option specifies that the server operates in UT1 time scale. (NTP protocol itself has no information about the time scale and assumes usage of UTC). This allows to synchronise time correctly with both UTC and UT1 time sources.
 
 {blank}:::
 
@@ -1460,6 +1462,23 @@ where the _/etc/chrony.tempcomp_ file could have
 Valid measurements with corresponding compensations are logged to the
 _tempcomp.log_ file if enabled by the <<log,*log tempcomp*>> directive.
 
+[[ut1]]*ut1* _source_ _parameter_::
+The *ut1* directive sets the local time scale to UT1. It has two mandatory parameters: _dUT1_ value source and a source-specific parameter.
++
+UT1 differs from UTC by the variable offset _dUT1_ (in seconds) which is defined as
++
+----
+dUT1 = UT1 - UTC
+----
++
+So, _dUT1_ value is needed in order to determine UT1 time. This value will be obtained from the specified source. The following sources are available:
++
+*const*:::
+Constant value. The parameter is the actual value.
++
+*bulletina*:::
+Value is calculated from Bulletin A csv file using the linear interpolation. The parameter is the path to csv file.
+
 === NTP server
 
 [[allow]]*allow* [*all*] [_subnet_]::
diff --git a/ntp_core.c b/ntp_core.c
index 44682ec..eac1a0d 100644
--- a/ntp_core.c
+++ b/ntp_core.c
@@ -223,6 +223,9 @@ struct NCR_Instance_Record {
 
   /* Report from last valid response and packet/timestamp statistics */
   RPT_NTPReport report;
+
+  /* Flag indicating the source is in UT1 mode */
+  int ut1;
 };
 
 typedef struct {
@@ -711,6 +714,8 @@ NCR_CreateInstance(NTP_Remote_Address *remote_addr, NTP_Source_Type type,
   zero_local_timestamp(&result->local_tx);
   result->burst_good_samples_to_go = 0;
   result->burst_total_samples_to_go = 0;
+
+  result->ut1 = params->ut1;
   
   NCR_ResetInstance(result);
 
@@ -2180,6 +2185,9 @@ process_response(NCR_Instance inst, int saved, NTP_Local_Address *local_addr,
     /* Apply configured correction */
     sample.offset += inst->offset_correction;
 
+    /* Apply dUT1 if the remote source works in another time scale */
+    sample.offset += CNF_GetUT1Offset() * (CNF_GetUT1() - inst->ut1);
+
     /* We treat the time of the sample as being midway through the local
        measurement period.  An analysis assuming constant relative
        frequency and zero network delay shows this is the only possible
diff --git a/reference.c b/reference.c
index 1ac6cb9..f7ae5fb 100644
--- a/reference.c
+++ b/reference.c
@@ -252,6 +252,9 @@ REF_Initialise(void)
   leap_timeout_id = 0;
   leap_in_progress = 0;
   leap_mode = CNF_GetLeapSecMode();
+  /* Ignore leap second in UT1 mode */
+  if (CNF_GetUT1())
+    leap_mode = REF_LeapModeIgnore;
   /* Switch to step mode if the system driver doesn't support leap */
   if (leap_mode == REF_LeapModeSystem && !LCL_CanSystemLeap())
     leap_mode = REF_LeapModeStep;
diff --git a/srcparams.h b/srcparams.h
index 31baed7..2abfb7a 100644
--- a/srcparams.h
+++ b/srcparams.h
@@ -65,6 +65,7 @@ typedef struct {
   double min_delay;
   double asymmetry;
   double offset;
+  int ut1;
 } SourceParameters;
 
 #define SRC_DEFAULT_PORT 123
-- 
2.44.0


-- 
To unsubscribe email chrony-dev-request@xxxxxxxxxxxxxxxxxxxx with "unsubscribe" in the subject.
For help email chrony-dev-request@xxxxxxxxxxxxxxxxxxxx with "help" in the subject.
Trouble?  Email listmaster@xxxxxxxxxxxxxxxxxxxx.


Mail converted by MHonArc 2.6.19+ http://listengine.tuxfamily.org/