]> git.ipfire.org Git - thirdparty/chrony.git/commitdiff
refclock: add SENSOR driver for OpenBSD timedelta sensors
authorAtanas Vladimirov <vladimirov.atanas@gmail.com>
Mon, 13 Jul 2026 19:52:19 +0000 (22:52 +0300)
committerMiroslav Lichvar <mlichvar@redhat.com>
Wed, 22 Jul 2026 10:10:07 +0000 (12:10 +0200)
OpenBSD does not provide the RFC 2783 PPS API. Instead, serial line
time sources attached with ldattach(8) (e.g. nmea(4)) and dedicated
radio clock drivers export a timedelta sensor through the hw.sensors
sysctl framework, containing the offset of the system clock relative
to the reference. When the line discipline is configured to timestamp
a PPS signal on a modem control line (e.g. ldattach -t dcd nmea), the
kernel timestamps the pulse at interrupt time, making the sensor
accurate to a few microseconds.

The device is allowed to be missing when chronyd starts (e.g. at
boot before ldattach attaches the line discipline) and is discovered
later at the driver polling interval, similarly to OpenBSD ntpd
rescanning its sensors periodically.

No pledge() changes are needed as hw.sensors sysctl reads are always
allowed on OpenBSD.

Example:

  refclock SENSOR nmea0 refid GPS precision 1e-7 prefer

configure
doc/chrony.conf.adoc
refclock.c
refclock_sensor.c [new file with mode: 0644]

index 4c385af5bdecd345df1238bf10c2e4052f691111..ef8892ed3c03ce3373b20d299c22ff85016913ab 100755 (executable)
--- a/configure
+++ b/configure
@@ -234,6 +234,7 @@ try_clockctl=0
 feat_scfilter=0
 try_seccomp=-1
 try_pledge=0
+try_sensors=0
 priv_ops=""
 feat_ipv6=1
 feat_phc=1
@@ -448,6 +449,7 @@ case $OPERATINGSYSTEM in
         try_setsched=1
         try_lockmem=1
         try_pledge=1
+        try_sensors=1
         add_def OPENBSD
         if [ $feat_droproot = "1" ]; then
           add_def FEAT_PRIVDROP
@@ -514,7 +516,7 @@ fi
 if [ $feat_refclock = "1" ]; then
   add_def FEAT_REFCLOCK
   EXTRA_OBJECTS="$EXTRA_OBJECTS refclock.o refclock_phc.o refclock_pps.o \
-    refclock_rtc.o refclock_shm.o refclock_sock.o"
+    refclock_rtc.o refclock_sensor.o refclock_shm.o refclock_sock.o"
 fi
 
 MYCC="$CC"
@@ -787,6 +789,18 @@ then
   add_def FEAT_PPS
 fi
 
+if [ $feat_refclock = "1" ] && [ $try_sensors = "1" ] && \
+  test_code 'OpenBSD sensors' 'sys/types.h sys/time.h sys/sysctl.h sys/sensors.h' '' '' '
+    struct sensordev sd;
+    struct sensor s;
+    int mib[5] = { CTL_HW, HW_SENSORS, 0, SENSOR_TIMEDELTA, 0 };
+    size_t len = sizeof (s);
+    return sysctl(mib, 5, &s, &len, NULL, 0) + sizeof (sd.xname) +
+           SENSOR_S_OK + SENSOR_FINVALID;'
+then
+  add_def FEAT_SENSOR
+fi
+
 if [ $feat_droproot = "1" ] && [ $try_libcap = "1" ] && \
   test_code \
     libcap \
index 3d2efb43d3d3331dcb6a673680aecf7a6057f4be..1b8b2879f5ae636e7e255001cad52cf5815db46f 100644 (file)
@@ -500,7 +500,7 @@ the driver-specific parameter using the *:* character.
 +
 This directive can be used multiple times to specify multiple reference clocks.
 +
-There are five drivers included in *chronyd*:
+There are six drivers included in *chronyd*:
 +
 *PPS*:::
 Driver for the kernel PPS (pulse per second) API. The parameter is the path to
@@ -636,6 +636,26 @@ Examples:
 refclock RTC /dev/rtc0:utc
 ----
 +
+*SENSOR*:::
+Driver for timedelta sensors provided by the OpenBSD *hw.sensors* framework.
+The parameter is the name of the sensor device (e.g. _nmea0_) whose first
+timedelta sensor should be used as a time source. Timedelta sensors are
+provided by serial line time sources attached with *ldattach(8)*, such as
+*nmea(4)*, and by dedicated radio clock devices. When the line discipline is
+configured to timestamp a PPS signal (e.g. *ldattach -t dcd nmea*), the kernel
+timestamps the pulse at interrupt time and the sensor provides a PPS-corrected
+offset of the system clock. Samples are ignored while the kernel marks the
+sensor as invalid or its status is not OK (e.g. the receiver lost its fix, or
+the PPS signal is missing). If the device does not exist yet when *chronyd*
+starts (e.g. it has not been attached with *ldattach(8)* yet), the driver
+waits for it to appear. This driver is available only on OpenBSD.
++
+Examples:
++
+----
+refclock SENSOR nmea0 refid GPS precision 1e-7 prefer
+----
++
 {blank}::
 The *refclock* directive supports the following options:
 +
index 3816d1249dc5b8e9cb17a6213292f2b09c8aa50f..062cc88618fcb9588f8885b3fe4f3aaf600aa3f1 100644 (file)
@@ -49,6 +49,7 @@ extern RefclockDriver RCL_SOCK_driver;
 extern RefclockDriver RCL_PPS_driver;
 extern RefclockDriver RCL_PHC_driver;
 extern RefclockDriver RCL_RTC_driver;
+extern RefclockDriver RCL_SENSOR_driver;
 
 struct FilterSample {
   double offset;
@@ -162,6 +163,8 @@ RCL_AddRefclock(RefclockParameters *params)
     inst->driver = &RCL_PHC_driver;
   } else if (strcmp(params->driver_name, "RTC") == 0) {
     inst->driver = &RCL_RTC_driver;
+  } else if (strcmp(params->driver_name, "SENSOR") == 0) {
+    inst->driver = &RCL_SENSOR_driver;
   } else {
     LOG_FATAL("unknown refclock driver %s", params->driver_name);
   }
diff --git a/refclock_sensor.c b/refclock_sensor.c
new file mode 100644 (file)
index 0000000..097a737
--- /dev/null
@@ -0,0 +1,207 @@
+/*
+  chronyd/chronyc - Programs for keeping computer clocks accurate.
+
+ **********************************************************************
+ * Copyright (C) Atanas Vladimirov  2026
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ **********************************************************************
+
+  =======================================================================
+
+  OpenBSD hw.sensors timedelta refclock driver.
+
+  Timedelta sensors are provided by serial line time sources attached
+  with ldattach(8), such as nmea(4), msts(4) and endrun(4), and by
+  dedicated radio clock devices like udcf(4) and mbg(4).  When the
+  nmea(4) line discipline is timestamped by a PPS signal on the DCD
+  line (ldattach -t dcd), the kernel timestamps the pulse at interrupt
+  time and the sensor is accurate to a few microseconds.
+
+  The sensor value is the difference between the system time and the
+  reference time in nanoseconds (positive if the system clock is ahead)
+  and the sensor timestamp is the system time of the measurement.
+
+  */
+
+#include "config.h"
+
+#include "refclock.h"
+
+#ifdef FEAT_SENSOR
+
+#include "sysincl.h"
+
+#include <sys/sysctl.h>
+#include <sys/sensors.h>
+
+#include "logging.h"
+#include "memory.h"
+#include "util.h"
+
+struct sensor_instance {
+  int dev;
+  char *name;
+  struct timeval last;
+};
+
+static int get_sensordev(int dev, struct sensordev *sd)
+{
+  int mib[3] = { CTL_HW, HW_SENSORS, dev };
+  size_t len = sizeof (*sd);
+
+  return sysctl(mib, 3, sd, &len, NULL, 0);
+}
+
+static int find_sensordev(const char *name)
+{
+  struct sensordev sd;
+  int dev;
+
+  for (dev = 0; ; dev++) {
+    if (get_sensordev(dev, &sd) < 0) {
+      if (errno == ENOENT)      /* past the last device */
+        break;
+      if (errno == ENXIO)       /* empty slot, keep scanning */
+        continue;
+      return -1;
+    }
+    if (strcmp(sd.xname, name) == 0)
+      return dev;
+  }
+
+  return -1;
+}
+
+/* Read the timedelta sensor after verifying the device name and presence of
+   the sensor, as the kernel reuses indices of detached devices and a
+   different device could take the place of the configured device */
+static int read_timedelta(int dev, const char *name, struct sensor *s)
+{
+  int mib[5] = { CTL_HW, HW_SENSORS, dev, SENSOR_TIMEDELTA, 0 };
+  size_t len = sizeof (*s);
+  struct sensordev sd;
+
+  if (dev < 0 || get_sensordev(dev, &sd) < 0 ||
+      strcmp(sd.xname, name) != 0 || sd.maxnumt[SENSOR_TIMEDELTA] < 1)
+    return -1;
+
+  return sysctl(mib, 5, s, &len, NULL, 0);
+}
+
+static int sensor_initialise(RCL_Instance instance)
+{
+  struct sensor_instance *sensor;
+  struct sensor s;
+  char *name;
+  int dev;
+
+  RCL_CheckDriverOptions(instance, NULL);
+
+  name = RCL_GetDriverParameter(instance);
+
+  /* The device may not be attached yet (e.g. chronyd started at boot
+     before ldattach(8)) - do not fail, wait for it to appear */
+  dev = find_sensordev(name);
+  if (dev < 0 || read_timedelta(dev, name, &s) < 0) {
+    LOG(LOGS_WARN, "Could not find timedelta sensor of device %s (waiting for it to appear)",
+        name);
+    dev = -1;
+  }
+
+  sensor = MallocNew(struct sensor_instance);
+  sensor->dev = dev;
+  sensor->name = Strdup(name);
+  sensor->last.tv_sec = 0;
+  sensor->last.tv_usec = 0;
+
+  RCL_SetDriverData(instance, sensor);
+  return 1;
+}
+
+static void sensor_finalise(RCL_Instance instance)
+{
+  struct sensor_instance *sensor;
+
+  sensor = (struct sensor_instance *)RCL_GetDriverData(instance);
+  Free(sensor->name);
+  Free(sensor);
+}
+
+static int sensor_poll(RCL_Instance instance)
+{
+  struct sensor_instance *sensor;
+  struct timespec sys_ts, ref_ts;
+  struct sensor s;
+  double offset;
+  int dev;
+
+  sensor = (struct sensor_instance *)RCL_GetDriverData(instance);
+
+  if (read_timedelta(sensor->dev, sensor->name, &s) < 0) {
+    /* the device may not be attached yet, or was reattached with
+       a different index */
+    dev = find_sensordev(sensor->name);
+    if (dev >= 0 && sensor->dev < 0)
+      LOG(LOGS_INFO, "Found sensor device %s", sensor->name);
+    sensor->dev = dev;
+    DEBUG_LOG("Could not read sensor %s", sensor->name);
+    return 0;
+  }
+
+  /* Ignore the sensor while it is invalid or degraded */
+  if (s.flags & SENSOR_FINVALID || s.status != SENSOR_S_OK) {
+    DEBUG_LOG("Ignoring sensor %s flags=%x status=%d",
+              sensor->name, (unsigned int)s.flags, (int)s.status);
+    return 0;
+  }
+
+  /* Wait for a new measurement */
+  if (s.tv.tv_sec == sensor->last.tv_sec &&
+      s.tv.tv_usec == sensor->last.tv_usec)
+    return 0;
+  sensor->last = s.tv;
+
+  if (!UTI_IsTimevalNormal(&s.tv)) {
+    DEBUG_LOG("Invalid timestamp from sensor %s", sensor->name);
+    return 0;
+  }
+
+  UTI_TimevalToTimespec(&s.tv, &sys_ts);
+
+  /* The sensor value is system time minus reference time in nanoseconds */
+  offset = -(double)s.value / 1.0e9;
+
+  if (!UTI_IsTimeOffsetSane(&sys_ts, offset))
+    return 0;
+
+  UTI_AddDoubleToTimespec(&sys_ts, offset, &ref_ts);
+
+  DEBUG_LOG("SENSOR %s sample offset=%.9f", sensor->name, offset);
+
+  return RCL_AddSample(instance, &sys_ts, &ref_ts, LEAP_Normal, 1);
+}
+
+RefclockDriver RCL_SENSOR_driver = {
+  sensor_initialise,
+  sensor_finalise,
+  sensor_poll
+};
+
+#else
+
+RefclockDriver RCL_SENSOR_driver = { NULL, NULL, NULL };
+
+#endif