[PATCH] refclock: add SENSORS driver for OpenBSD timedelta sensors

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


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.
---
 configure            |  14 ++-
 doc/chrony.conf.adoc |  20 ++++-
 refclock.c           |   3 +
 refclock_sensors.c   | 198 +++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 233 insertions(+), 2 deletions(-)
 create mode 100644 refclock_sensors.c

diff --git a/configure b/configure
index 4c385af..d1f6b15 100755
--- a/configure
+++ b/configure
@@ -514,7 +514,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_sensors.o refclock_shm.o refclock_sock.o"
 fi
 
 MYCC="$CC"
@@ -787,6 +787,18 @@ then
   add_def FEAT_PPS
 fi
 
+if [ $feat_refclock = "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_SENSORS
+fi
+
 if [ $feat_droproot = "1" ] && [ $try_libcap = "1" ] && \
   test_code \
     libcap \
diff --git a/doc/chrony.conf.adoc b/doc/chrony.conf.adoc
index 414c9c5..7b02436 100644
--- a/doc/chrony.conf.adoc
+++ b/doc/chrony.conf.adoc
@@ -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,24 @@ Examples:
 refclock RTC /dev/rtc0:utc
 ----
 +
+*SENSORS*:::
+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). This driver is available only on OpenBSD.
++
+Examples:
++
+----
+refclock SENSORS nmea0 refid GPS precision 1e-7 prefer
+----
++
 {blank}::
 The *refclock* directive supports the following options:
 +
diff --git a/refclock.c b/refclock.c
index d73e3c0..cab49cf 100644
--- a/refclock.c
+++ b/refclock.c
@@ -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_SENSORS_driver;
 
 struct FilterSample {
   double offset;
@@ -163,6 +164,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, "SENSORS") == 0) {
+    inst->driver = &RCL_SENSORS_driver;
   } else {
     LOG_FATAL("unknown refclock driver %s", params->driver_name);
   }
diff --git a/refclock_sensors.c b/refclock_sensors.c
new file mode 100644
index 0000000..51c8c91
--- /dev/null
+++ b/refclock_sensors.c
@@ -0,0 +1,198 @@
+/*
+  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_SENSORS
+
+#include "sysincl.h"
+
+#include <sys/sysctl.h>
+#include <sys/sensors.h>
+
+#include "logging.h"
+#include "memory.h"
+#include "util.h"
+
+struct sensors_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)
+{
+  struct sensordev sd;
+  int mib[5] = { CTL_HW, HW_SENSORS, dev, SENSOR_TIMEDELTA, 0 };
+  size_t len = sizeof (*s);
+
+  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 sensors_initialise(RCL_Instance instance)
+{
+  struct sensors_instance *sensors;
+  struct sensor s;
+  char *name;
+  int dev;
+
+  RCL_CheckDriverOptions(instance, NULL);
+
+  name = RCL_GetDriverParameter(instance);
+  if (!name || name[0] == '\0')
+    LOG_FATAL("Missing sensor device");
+
+  dev = find_sensordev(name);
+  if (dev < 0)
+    LOG_FATAL("Could not find sensor device %s", name);
+
+  if (read_timedelta(dev, name, &s) < 0)
+    LOG_FATAL("Could not read timedelta sensor of device %s", name);
+
+  sensors = MallocNew(struct sensors_instance);
+  sensors->dev = dev;
+  sensors->name = name;
+  sensors->last.tv_sec = 0;
+  sensors->last.tv_usec = 0;
+
+  RCL_SetDriverData(instance, sensors);
+  return 1;
+}
+
+static void sensors_finalise(RCL_Instance instance)
+{
+  Free(RCL_GetDriverData(instance));
+}
+
+static int sensors_poll(RCL_Instance instance)
+{
+  struct sensors_instance *sensors;
+  struct timespec sys_ts, ref_ts;
+  struct sensor s;
+  double offset;
+
+  sensors = (struct sensors_instance *)RCL_GetDriverData(instance);
+
+  if (read_timedelta(sensors->dev, sensors->name, &s) < 0) {
+    /* the device may have been reattached with a different index */
+    sensors->dev = find_sensordev(sensors->name);
+    DEBUG_LOG("Could not read sensor %s", sensors->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",
+              sensors->name, (unsigned int)s.flags, (int)s.status);
+    return 0;
+  }
+
+  /* Wait for a new measurement */
+  if (s.tv.tv_sec == sensors->last.tv_sec &&
+      s.tv.tv_usec == sensors->last.tv_usec)
+    return 0;
+  sensors->last = s.tv;
+
+  if (!UTI_IsTimevalNormal(&s.tv)) {
+    DEBUG_LOG("Invalid timestamp from sensor %s", sensors->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("SENSORS %s sample offset=%.9f", sensors->name, offset);
+
+  return RCL_AddSample(instance, &sys_ts, &ref_ts, LEAP_Normal, 1);
+}
+
+RefclockDriver RCL_SENSORS_driver = {
+  sensors_initialise,
+  sensors_finalise,
+  sensors_poll
+};
+
+#else
+
+RefclockDriver RCL_SENSORS_driver = { NULL, NULL, NULL };
+
+#endif
-- 
2.43.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/