From: Atanas Vladimirov Date: Mon, 13 Jul 2026 19:52:19 +0000 (+0300) Subject: refclock: add SENSOR driver for OpenBSD timedelta sensors X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=cff6f1ca2a46ebd8d348eaa71054740e9934fa7d;p=thirdparty%2Fchrony.git refclock: add SENSOR driver for OpenBSD timedelta sensors 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 --- diff --git a/configure b/configure index 4c385af5..ef8892ed 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 3d2efb43..1b8b2879 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 3816d124..062cc886 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 00000000..097a7375 --- /dev/null +++ b/refclock_sensor.c @@ -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 +#include + +#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