| [PATCH v3] refclock: add SENSOR driver for OpenBSD timedelta sensors |
[ Thread Index |
Date Index
| More chrony.tuxfamily.org/chrony-dev Archives
]
- Subject: [PATCH v3] refclock: add SENSOR driver for OpenBSD timedelta sensors
- From: Atanas Vladimirov <vladimirov.atanas@xxxxxxxxx>
- Date: Mon, 13 Jul 2026 22:52:19 +0300
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 | 16 +++-
doc/chrony.conf.adoc | 22 ++++-
refclock.c | 3 +
refclock_sensor.c | 211 +++++++++++++++++++++++++++++++++++++++++++
4 files changed, 250 insertions(+), 2 deletions(-)
create mode 100644 refclock_sensor.c
diff --git a/configure b/configure
index 4c385af..ef8892e 100755
--- 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 \
diff --git a/doc/chrony.conf.adoc b/doc/chrony.conf.adoc
index 3d2efb4..1b8b287 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,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:
+
diff --git a/refclock.c b/refclock.c
index 3816d12..062cc88 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_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
index 0000000..ad54d5a
--- /dev/null
+++ b/refclock_sensor.c
@@ -0,0 +1,211 @@
+/*
+ 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);
+ if (!name || name[0] == '\0') {
+ LOG(LOGS_ERR, "Missing sensor device");
+ return 0;
+ }
+
+ /* 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
--
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.