From 867239c7b80d1e2f0db5757edf83d8c9513ebe30 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 8 Feb 2026 15:16:05 +0100 Subject: [PATCH 1/5] package/feature-gps: new feature package Enable gpsd and override package defaults to integrate in Infix. - Sync with rules disabled downstream in Debian project to prevent clashes with common vendor/device ids used for other device types - Start gpsd instance with '-n' to ensure /dev/shm is udpated for chrony Signed-off-by: Joachim Wiberg --- configs/aarch64_defconfig | 4 ++ configs/aarch64_minimal_defconfig | 4 ++ configs/arm_defconfig | 4 ++ configs/arm_minimal_defconfig | 4 ++ configs/riscv64_defconfig | 4 ++ configs/x86_64_defconfig | 4 ++ configs/x86_64_minimal_defconfig | 4 ++ package/Config.in | 1 + package/feature-gps/25-gpsd.rules | 65 ++++++++++++++++++++++++++++++ package/feature-gps/Config.in | 34 ++++++++++++++++ package/feature-gps/feature-gps.mk | 21 ++++++++++ package/feature-gps/gpsd.default | 1 + 12 files changed, 150 insertions(+) create mode 100644 package/feature-gps/25-gpsd.rules create mode 100644 package/feature-gps/Config.in create mode 100644 package/feature-gps/feature-gps.mk create mode 100644 package/feature-gps/gpsd.default diff --git a/configs/aarch64_defconfig b/configs/aarch64_defconfig index ccc59f03d..0d9ff6920 100644 --- a/configs/aarch64_defconfig +++ b/configs/aarch64_defconfig @@ -42,6 +42,9 @@ BR2_PACKAGE_DBUS_GLIB=y BR2_PACKAGE_DBUS_TRIGGERD=y BR2_PACKAGE_EUDEV_RULES_GEN=y # BR2_PACKAGE_EUDEV_ENABLE_HWDB is not set +BR2_PACKAGE_GPSD_DEVICES="/dev/gps0" +BR2_PACKAGE_GPSD_MAX_CLIENT_VALUE=2 +BR2_PACKAGE_GPSD_MAX_DEV_VALUE=1 BR2_PACKAGE_GPTFDISK=y BR2_PACKAGE_GPTFDISK_SGDISK=y BR2_PACKAGE_MDIO_TOOLS=y @@ -146,6 +149,7 @@ INFIX_DESC="Infix is an immutable, friendly, and secure operating system that tu INFIX_HOME="https://github.com/kernelkit/infix/" INFIX_DOC="https://kernelkit.org/infix/" INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" +BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y BR2_PACKAGE_FEATURE_WIFI_REALTEK=y BR2_PACKAGE_CONFD=y diff --git a/configs/aarch64_minimal_defconfig b/configs/aarch64_minimal_defconfig index c8f5f3211..af01486f6 100644 --- a/configs/aarch64_minimal_defconfig +++ b/configs/aarch64_minimal_defconfig @@ -42,6 +42,9 @@ BR2_PACKAGE_DBUS_GLIB=y BR2_PACKAGE_DBUS_TRIGGERD=y BR2_PACKAGE_EUDEV_RULES_GEN=y # BR2_PACKAGE_EUDEV_ENABLE_HWDB is not set +BR2_PACKAGE_GPSD_DEVICES="/dev/gps0" +BR2_PACKAGE_GPSD_MAX_CLIENT_VALUE=2 +BR2_PACKAGE_GPSD_MAX_DEV_VALUE=1 BR2_PACKAGE_GPTFDISK=y BR2_PACKAGE_GPTFDISK_SGDISK=y BR2_PACKAGE_MDIO_TOOLS=y @@ -124,6 +127,7 @@ INFIX_DESC="Infix is an immutable, friendly, and secure operating system that tu INFIX_HOME="https://github.com/kernelkit/infix/" INFIX_DOC="https://kernelkit.org/infix/" INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" +BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_CONFD=y BR2_PACKAGE_CONFD_TEST_MODE=y BR2_PACKAGE_GENCERT=y diff --git a/configs/arm_defconfig b/configs/arm_defconfig index 20e286d07..d0a8db975 100644 --- a/configs/arm_defconfig +++ b/configs/arm_defconfig @@ -44,6 +44,9 @@ BR2_PACKAGE_DBUS_GLIB=y BR2_PACKAGE_DBUS_TRIGGERD=y BR2_PACKAGE_EUDEV_RULES_GEN=y # BR2_PACKAGE_EUDEV_ENABLE_HWDB is not set +BR2_PACKAGE_GPSD_DEVICES="/dev/gps0" +BR2_PACKAGE_GPSD_MAX_CLIENT_VALUE=2 +BR2_PACKAGE_GPSD_MAX_DEV_VALUE=1 BR2_PACKAGE_GPTFDISK=y BR2_PACKAGE_GPTFDISK_SGDISK=y BR2_PACKAGE_MDIO_TOOLS=y @@ -142,6 +145,7 @@ INFIX_DESC="Infix is an immutable, friendly, and secure operating system that tu INFIX_HOME="https://github.com/kernelkit/infix/" INFIX_DOC="https://kernelkit.org/infix/" INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" +BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y BR2_PACKAGE_FEATURE_WIFI_REALTEK=y BR2_PACKAGE_CONFD=y diff --git a/configs/arm_minimal_defconfig b/configs/arm_minimal_defconfig index 98a211e98..0eac09603 100644 --- a/configs/arm_minimal_defconfig +++ b/configs/arm_minimal_defconfig @@ -44,6 +44,9 @@ BR2_PACKAGE_DBUS_GLIB=y BR2_PACKAGE_DBUS_TRIGGERD=y BR2_PACKAGE_EUDEV_RULES_GEN=y # BR2_PACKAGE_EUDEV_ENABLE_HWDB is not set +BR2_PACKAGE_GPSD_DEVICES="/dev/gps0" +BR2_PACKAGE_GPSD_MAX_CLIENT_VALUE=2 +BR2_PACKAGE_GPSD_MAX_DEV_VALUE=1 BR2_PACKAGE_GPTFDISK=y BR2_PACKAGE_GPTFDISK_SGDISK=y BR2_PACKAGE_MDIO_TOOLS=y @@ -124,6 +127,7 @@ INFIX_DESC="Infix is an immutable, friendly, and secure operating system that tu INFIX_HOME="https://github.com/kernelkit/infix/" INFIX_DOC="https://kernelkit.org/infix/" INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" +BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_CONFD=y BR2_PACKAGE_CONFD_TEST_MODE=y BR2_PACKAGE_GENCERT=y diff --git a/configs/riscv64_defconfig b/configs/riscv64_defconfig index f496f4b46..ef5f05dc2 100644 --- a/configs/riscv64_defconfig +++ b/configs/riscv64_defconfig @@ -53,6 +53,9 @@ BR2_PACKAGE_DBUS_GLIB=y BR2_PACKAGE_DBUS_TRIGGERD=y BR2_PACKAGE_EUDEV_RULES_GEN=y # BR2_PACKAGE_EUDEV_ENABLE_HWDB is not set +BR2_PACKAGE_GPSD_DEVICES="/dev/gps0" +BR2_PACKAGE_GPSD_MAX_CLIENT_VALUE=2 +BR2_PACKAGE_GPSD_MAX_DEV_VALUE=1 BR2_PACKAGE_GPTFDISK=y BR2_PACKAGE_GPTFDISK_SGDISK=y BR2_PACKAGE_MDIO_TOOLS=y @@ -173,6 +176,7 @@ INFIX_DESC="Infix is an immutable, friendly, and secure operating system that tu INFIX_HOME="https://github.com/kernelkit/infix/" INFIX_DOC="https://kernelkit.org/infix/" INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" +BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_FEATURE_WIFI=y BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y BR2_PACKAGE_FEATURE_WIFI_REALTEK=y diff --git a/configs/x86_64_defconfig b/configs/x86_64_defconfig index 431b28397..c64156302 100644 --- a/configs/x86_64_defconfig +++ b/configs/x86_64_defconfig @@ -42,6 +42,9 @@ BR2_PACKAGE_DBUS_GLIB=y BR2_PACKAGE_DBUS_TRIGGERD=y BR2_PACKAGE_EUDEV_RULES_GEN=y # BR2_PACKAGE_EUDEV_ENABLE_HWDB is not set +BR2_PACKAGE_GPSD_DEVICES="/dev/gps0" +BR2_PACKAGE_GPSD_MAX_CLIENT_VALUE=2 +BR2_PACKAGE_GPSD_MAX_DEV_VALUE=1 BR2_PACKAGE_GPTFDISK=y BR2_PACKAGE_GPTFDISK_SGDISK=y BR2_PACKAGE_RNG_TOOLS=y @@ -145,6 +148,7 @@ INFIX_DESC="Infix is an immutable, friendly, and secure operating system that tu INFIX_HOME="https://github.com/kernelkit/infix/" INFIX_DOC="https://kernelkit.org/infix/" INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" +BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_FEATURE_WIFI=y BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y BR2_PACKAGE_FEATURE_WIFI_REALTEK=y diff --git a/configs/x86_64_minimal_defconfig b/configs/x86_64_minimal_defconfig index 3398654c3..2fa921ecf 100644 --- a/configs/x86_64_minimal_defconfig +++ b/configs/x86_64_minimal_defconfig @@ -42,6 +42,9 @@ BR2_PACKAGE_DBUS_GLIB=y BR2_PACKAGE_DBUS_TRIGGERD=y BR2_PACKAGE_EUDEV_RULES_GEN=y # BR2_PACKAGE_EUDEV_ENABLE_HWDB is not set +BR2_PACKAGE_GPSD_DEVICES="/dev/gps0" +BR2_PACKAGE_GPSD_MAX_CLIENT_VALUE=2 +BR2_PACKAGE_GPSD_MAX_DEV_VALUE=1 BR2_PACKAGE_GPTFDISK=y BR2_PACKAGE_GPTFDISK_SGDISK=y BR2_PACKAGE_RNG_TOOLS=y @@ -123,6 +126,7 @@ INFIX_DESC="Infix is an immutable, friendly, and secure operating system that tu INFIX_HOME="https://github.com/kernelkit/infix/" INFIX_DOC="https://kernelkit.org/infix/" INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" +BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_CONFD=y BR2_PACKAGE_CONFD_TEST_MODE=y BR2_PACKAGE_GENCERT=y diff --git a/package/Config.in b/package/Config.in index 71cf73cc7..3ccb35d03 100644 --- a/package/Config.in +++ b/package/Config.in @@ -1,6 +1,7 @@ menu "Packages" comment "Hardware Support" +source "$BR2_EXTERNAL_INFIX_PATH/package/feature-gps/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/feature-wifi/Config.in" comment "Software Packages" diff --git a/package/feature-gps/25-gpsd.rules b/package/feature-gps/25-gpsd.rules new file mode 100644 index 000000000..906936b46 --- /dev/null +++ b/package/feature-gps/25-gpsd.rules @@ -0,0 +1,65 @@ +# Override udev rules for gpsd +# +# This file is Copyright 2010 by the GPSD project +# SPDX-License-Identifier: BSD-2-clause + +SUBSYSTEM!="tty", GOTO="gpsd_rules_end" + +# Prolific Technology, Inc. PL2303 Serial Port [linux module: pl2303] +# !!! rule disabled in Debian as it matches too many other devices +# ATTRS{idVendor}=="067b", ATTRS{idProduct}=="2303", SYMLINK+="gps%n", RUN+="/usr/lib/udev/gpsd.hotplug" +# ATEN International Co., Ltd UC-232A Serial Port [linux module: pl2303] +ATTRS{idVendor}=="0557", ATTRS{idProduct}=="2008", SYMLINK+="gps%n", RUN+="/usr/lib/udev/gpsd.hotplug" + +# PS-360 OEM (GPS sold with MS Street and Trips 2005) [linux module: pl2303] +ATTRS{idVendor}=="067b", ATTRS{idProduct}=="aaa0", SYMLINK+="gps%n", RUN+="/usr/lib/udev/gpsd.hotplug" + +# FTDI 8U232AM / FT232 [linux module: ftdi_sio] +# !!! rule disabled in Debian as it matches too many other devices +# ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", SYMLINK+="gps%n", RUN+="/usr/lib/udev/gpsd.hotplug" + +# Cypress M8/CY7C64013 (Delorme uses these) [linux module: cypress_m8] +ATTRS{idVendor}=="1163", ATTRS{idProduct}=="0100", SYMLINK+="gps%n", RUN+="/usr/lib/udev/gpsd.hotplug" + +# Cypress M8/CY7C64013 (DeLorme LT-40) +ATTRS{idVendor}=="1163", ATTRS{idProduct}=="0200", SYMLINK+="gps%n", RUN+="/usr/lib/udev/gpsd.hotplug" + +# Garmin International GPSmap, various models (tested with Garmin GPS 18 USB) [linux module: garmin_gps] +ATTRS{idVendor}=="091e", ATTRS{idProduct}=="0003", SYMLINK+="gps%n", RUN+="/usr/lib/udev/gpsd.hotplug" + +# Cygnal Integrated Products, Inc. CP210x Composite Device (Used by Holux m241 and Wintec grays2 wbt-201) [linux module: cp210x] +# !!! rule disabled in Debian as it matches too many other devices +#ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", SYMLINK+="gps%n", RUN+="/usr/lib/udev/gpsd.hotplug" + +# Cygnal Integrated Products, Inc. [linux module: cp210x] +# !!! rule disabled in Debian as it matches too many other devices +#ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea71", SYMLINK+="gps%n", RUN+="/usr/lib/udev/gpsd.hotplug" + +# u-blox AG, u-blox 5 (tested with Navilock NL-402U) [linux module: cdc_acm] +ATTRS{idVendor}=="1546", ATTRS{idProduct}=="01a5", SYMLINK+="gps%n", RUN+="/usr/lib/udev/gpsd.hotplug" + +# u-blox AG, u-blox 6 (tested with GNSS Evaluation Kit TCXO) [linux module: cdc_acm] +ATTRS{idVendor}=="1546", ATTRS{idProduct}=="01a6", SYMLINK+="gps%n", RUN+="/usr/lib/udev/gpsd.hotplug" + +# u-blox AG, u-blox 7 [linux module: cdc_acm] +ATTRS{idVendor}=="1546", ATTRS{idProduct}=="01a7", SYMLINK+="gps%n", RUN+="/usr/lib/udev/gpsd.hotplug" + +# u-blox AG, u-blox 8 (tested with GNSS Evaluation Kit EKV-M8N) [linux module: cdc_acm] +ATTRS{idVendor}=="1546", ATTRS{idProduct}=="01a8", SYMLINK+="gps%n", RUN+="/usr/lib/udev/gpsd.hotplug" + +# u-blox AG, u-blox 9 (tested with GNSS Evaluation Kit C099-F9P) [linux module: cdc_acm] +ATTRS{idVendor}=="1546", ATTRS{idProduct}=="01a9", SYMLINK+="gps%n", RUN+="/usr/lib/udev/gpsd.hotplug" + +# MediaTek (tested with HOLUX M-1200E) [linux module: cdc_acm] +ATTRS{idVendor}=="0e8d", ATTRS{idProduct}=="3329", SYMLINK+="gps%n", RUN+="/usr/lib/udev/gpsd.hotplug" + +# Telit wireless solutions (tested with HE910G) [linux module: cdc_acm] +ATTRS{interface}=="Telit Wireless Module Port", ATTRS{bInterfaceNumber}=="06", SYMLINK+="gps%n", RUN+="/usr/lib/udev/gpsd.hotplug" + +# u-blox AG, u-blox 8 (tested with u-blox8 GNSS Mouse Receiver / GR-801) [linux module: cdc_acm] +# !!! rule disabled in Debian as it matches too many other devices +#ATTRS{idVendor}=="067b", ATTRS{idProduct}=="2303", SYMLINK+="gps%n", RUN+="/usr/lib/udev/gpsd.hotplug" + +ACTION=="remove", RUN+="/usr/lib/udev/gpsd.hotplug" + +LABEL="gpsd_rules_end" diff --git a/package/feature-gps/Config.in b/package/feature-gps/Config.in new file mode 100644 index 000000000..a222e5218 --- /dev/null +++ b/package/feature-gps/Config.in @@ -0,0 +1,34 @@ +config BR2_PACKAGE_FEATURE_GPS + bool "Feature GPS/GNSS" + select BR2_PACKAGE_GPSD + select BR2_PACKAGE_GPSD_MAX_CLIENT + select BR2_PACKAGE_GPSD_MAX_DEV + select BR2_PACKAGE_GPSD_SQUELCH + select BR2_PACKAGE_GPSD_ASHTECH + select BR2_PACKAGE_GPSD_EARTHMATE + select BR2_PACKAGE_GPSD_EVERMORE + select BR2_PACKAGE_GPSD_FURY + select BR2_PACKAGE_GPSD_FV18 + select BR2_PACKAGE_GPSD_GARMIN + select BR2_PACKAGE_GPSD_GARMIN_SIMPLE_TXT + select BR2_PACKAGE_GPSD_GEOSTAR + select BR2_PACKAGE_GPSD_GPSCLOCK + select BR2_PACKAGE_GPSD_GREIS + select BR2_PACKAGE_GPSD_ISYNC + select BR2_PACKAGE_GPSD_ITRAX + select BR2_PACKAGE_GPSD_NMEA2000 + select BR2_PACKAGE_GPSD_OCEANSERVER + select BR2_PACKAGE_GPSD_ONCORE + select BR2_PACKAGE_GPSD_RTCM104V2 + select BR2_PACKAGE_GPSD_RTCM104V3 + select BR2_PACKAGE_GPSD_SIRF + select BR2_PACKAGE_GPSD_SKYTRAQ + select BR2_PACKAGE_GPSD_SUPERSTAR2 + select BR2_PACKAGE_GPSD_TRIMBLE_TSIP + select BR2_PACKAGE_GPSD_TRIPMATE + select BR2_PACKAGE_GPSD_TRUE_NORTH + select BR2_PACKAGE_GPSD_UBX + help + Enables GPS/GNSS support in Infix. Includes gpsd with all + receiver protocol drivers and kernel USB ACM support for + common USB GPS receivers. diff --git a/package/feature-gps/feature-gps.mk b/package/feature-gps/feature-gps.mk new file mode 100644 index 000000000..6da640c97 --- /dev/null +++ b/package/feature-gps/feature-gps.mk @@ -0,0 +1,21 @@ +################################################################################ +# +# GPS/GNSS support +# +################################################################################ + +FEATURE_GPS_PACKAGE_VERSION = 1.0 +FEATURE_GPS_PACKAGE_LICENSE = MIT + +define FEATURE_GPS_INSTALL_TARGET_CMDS + $(INSTALL) -D -m 0644 $(FEATURE_GPS_PKGDIR)/gpsd.default \ + $(TARGET_DIR)/etc/default/gpsd + $(INSTALL) -D -m 0644 $(FEATURE_GPS_PKGDIR)/25-gpsd.rules \ + $(TARGET_DIR)/usr/lib/udev/rules.d/25-gpsd.rules +endef + +define FEATURE_GPS_LINUX_CONFIG_FIXUPS + $(call KCONFIG_SET_OPT,CONFIG_USB_ACM,m) +endef + +$(eval $(generic-package)) diff --git a/package/feature-gps/gpsd.default b/package/feature-gps/gpsd.default new file mode 100644 index 000000000..2fba46864 --- /dev/null +++ b/package/feature-gps/gpsd.default @@ -0,0 +1 @@ +GPSD_OPTIONS="-n" From e5008c3d9e77758a5996aee57f484f849938ce71 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 8 Feb 2026 10:01:10 +0100 Subject: [PATCH 2/5] confd: add gps support to ietf-hardware and ietf-ntp Signed-off-by: Joachim Wiberg --- src/confd/src/hardware.c | 6 + src/confd/src/ntp.c | 77 ++++++++- src/confd/yang/confd.inc | 4 +- src/confd/yang/confd/infix-hardware.yang | 139 +++++++++++++++ ...19.yang => infix-hardware@2026-02-08.yang} | 0 src/confd/yang/confd/infix-ntp.yang | 163 ++++++++++++++++++ ...5-12-03.yang => infix-ntp@2026-02-08.yang} | 0 7 files changed, 385 insertions(+), 4 deletions(-) rename src/confd/yang/confd/{infix-hardware@2026-01-19.yang => infix-hardware@2026-02-08.yang} (100%) rename src/confd/yang/confd/{infix-ntp@2025-12-03.yang => infix-ntp@2026-02-08.yang} (100%) diff --git a/src/confd/src/hardware.c b/src/confd/src/hardware.c index 6d441fe7f..e7c304500 100644 --- a/src/confd/src/hardware.c +++ b/src/confd/src/hardware.c @@ -150,6 +150,12 @@ static int hardware_cand_infer_class(json_t *root, sr_session_ctx_t *session, co inferred.data.string_val = "infix-hardware:wifi"; err = srx_set_item(session, &inferred, 0, "%s/class", xpath); } + + if (!fnmatch("gps+([0-9])", name, FNM_EXTMATCH)) { + inferred.data.string_val = "infix-hardware:gps"; + err = srx_set_item(session, &inferred, 0, "%s/class", xpath); + } + out_free_name: free(name); out_free_xpath: diff --git a/src/confd/src/ntp.c b/src/confd/src/ntp.c index 9813a6f2e..82b98591b 100644 --- a/src/confd/src/ntp.c +++ b/src/confd/src/ntp.c @@ -134,12 +134,85 @@ static int change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd } fprintf(fp, "\n"); - /* Reference clock (local stratum) - fallback time source */ + /* Reference clock configuration */ refclock = lydx_get_child(ntp, "refclock-master"); if (refclock) { + struct lyd_node *source; int stratum = atoi(lydx_get_cattr(refclock, "master-stratum")); + bool has_gps_sources = false; + + /* + * GPS/GNSS reference clock sources via gpsd SHM + * + * Each GPS hardware component (gps0, gps1, ...) maps to: + * - SHM unit 2*N for GPS time (coarse, ~100ms) + * - SHM unit 2*N+1 for PPS time (precise, ~1us) if enabled + * + * The mapping is based on gpsd's convention. + */ + LYX_LIST_FOR_EACH(lyd_child(refclock), source, "source") { + const char *receiver, *refid, *poll_str, *precision_str; + const char *offset_str, *delay_str; + bool prefer, pps; + int unit; + + receiver = lydx_get_cattr(source, "receiver"); + if (!receiver) + continue; + + /* Extract unit number from receiver name (gps0 -> 0, gps1 -> 1, ...) */ + if (sscanf(receiver, "gps%d", &unit) != 1) { + ERROR("Invalid GPS receiver name: %s (expected gpsN)", receiver); + continue; + } + + refid = lydx_get_cattr(source, "refid"); + poll_str = lydx_get_cattr(source, "poll"); + precision_str = lydx_get_cattr(source, "precision"); + offset_str = lydx_get_cattr(source, "offset"); + delay_str = lydx_get_cattr(source, "delay"); + prefer = lydx_get_bool(source, "prefer"); + pps = lydx_get_bool(source, "pps"); + + if (!has_gps_sources) { + fprintf(fp, "# GPS/GNSS reference clock sources (via gpsd SHM)\n"); + has_gps_sources = true; + } + + /* GPS time source: SHM unit 2*N */ + fprintf(fp, "refclock SHM %d", unit * 2); + if (poll_str) + fprintf(fp, " poll %s", poll_str); + if (refid) + fprintf(fp, " refid %s", refid); + if (precision_str) + fprintf(fp, " precision %s", precision_str); + if (offset_str && atof(offset_str) != 0.0) + fprintf(fp, " offset %s", offset_str); + if (delay_str && atof(delay_str) != 0.0) + fprintf(fp, " delay %s", delay_str); + if (prefer) + fprintf(fp, " prefer"); + fprintf(fp, "\n"); + + /* PPS time source: SHM unit 2*N+1 (if enabled) */ + if (pps) { + fprintf(fp, "refclock SHM %d", unit * 2 + 1); + fprintf(fp, " poll %s", poll_str ? poll_str : "-6"); + fprintf(fp, " refid PPS"); + fprintf(fp, " precision 0.000001"); + if (prefer) + fprintf(fp, " prefer"); + /* PPS needs a time source for initial lock */ + fprintf(fp, " lock %s", refid ? refid : "GPS"); + fprintf(fp, "\n"); + } + } + + if (has_gps_sources) + fprintf(fp, "\n"); - /* Only configure local clock if stratum is valid (1-15) */ + /* Local clock fallback - only if stratum is valid (1-15) */ if (stratum >= 1 && stratum <= 15) { fprintf(fp, "# Local reference clock - fallback stratum %d source\n", stratum); fprintf(fp, "local stratum %d orphan\n\n", stratum); diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index 10ef69d5a..8efe9e3e1 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -27,7 +27,7 @@ MODULES=( "infix-syslog@2025-11-17.yang" "iana-hardware@2018-03-13.yang" "ietf-hardware@2018-03-13.yang -e hardware-state -e hardware-sensor" - "infix-hardware@2026-01-19.yang" + "infix-hardware@2026-02-08.yang" "ieee802-dot1q-types@2022-10-29.yang" "infix-ip@2025-11-02.yang" "infix-if-type@2026-01-07.yang" @@ -51,6 +51,6 @@ MODULES=( "ietf-crypto-types -e cleartext-symmetric-keys" "infix-crypto-types@2025-11-09.yang" "ietf-keystore -e symmetric-keys" - "infix-ntp@2025-12-03.yang" + "infix-ntp@2026-02-08.yang" "infix-keystore@2025-12-17.yang" ) diff --git a/src/confd/yang/confd/infix-hardware.yang b/src/confd/yang/confd/infix-hardware.yang index 09b7b2a2b..b28ada610 100644 --- a/src/confd/yang/confd/infix-hardware.yang +++ b/src/confd/yang/confd/infix-hardware.yang @@ -21,6 +21,10 @@ module infix-hardware { contact "kernelkit@googlegroups.com"; description "Vital Product Data augmentation of ieee-hardware and deviations."; + revision 2026-02-08 { + description "Add GPS/GNSS receiver hardware class and container for time sync."; + reference "internal"; + } revision 2026-01-19 { description "Add probe-timeout for slow USB WiFi dongles."; reference "internal"; @@ -63,6 +67,15 @@ module infix-hardware { WiFi radios are hardware components with class 'ih:wifi'."; } + typedef gps-receiver-ref { + type leafref { + path "/iehw:hardware/iehw:component/iehw:name"; + } + description + "Reference to a GPS/GNSS receiver hardware component. + GPS receivers are hardware components with class 'ih:gps'."; + } + typedef wifi-band { type enumeration { enum "2.4GHz" { @@ -95,6 +108,11 @@ module infix-hardware { description "This identity is used to describe a WiFi radio/PHY"; } + identity gps { + base iahw:hardware-class; + description "GPS/GNSS receiver for time synchronization"; + } + deviation "/iehw:hardware/iehw:component/iehw:state/iehw:admin-state" { deviate add { must ". = 'locked' or . = 'unlocked'" { @@ -510,5 +528,126 @@ module infix-hardware { } } } + + /* + * GPS/GNSS Receiver configuration (when class = 'ih:gps') + */ + + container gps-receiver { + when "derived-from-or-self(../iehw:class, 'ih:gps')"; + presence "GPS receiver configuration"; + description + "GPS/GNSS receiver configuration and operational data. + + This container is present when the hardware component represents + a GPS/GNSS receiver (class 'ih:gps'). GPS receivers provide + precision time via gpsd, which chronyd can use as a reference + clock through shared memory (SHM). + + The hardware component name (e.g., 'gps0') determines: + - Device path: /dev/gps0 (symlink managed by udev) + - SHM unit: 0 for GPS time, 1 for PPS (derived from gpsN index) + + Supported GNSS constellations depend on the hardware: + - GPS (US) + - GLONASS (Russia) + - Galileo (EU) + - BeiDou (China)"; + + /* + * Operational state from gpsd + */ + + leaf device { + config false; + type string; + description + "Actual device path being used (e.g., /dev/ttyUSB0, /dev/ttyACM0). + This is the physical device that /dev/gpsN symlinks to."; + } + + leaf driver { + config false; + type string; + description + "GPS driver/protocol in use (e.g., 'u-blox', 'NMEA', 'SiRF')."; + } + + leaf activated { + config false; + type boolean; + description + "Whether gpsd has successfully activated this GPS device."; + } + + leaf satellites-visible { + config false; + type uint8; + description + "Number of satellites currently visible to the receiver."; + } + + leaf satellites-used { + config false; + type uint8; + description + "Number of satellites being used for the fix."; + } + + leaf fix-mode { + config false; + type enumeration { + enum "none" { + description "No fix available"; + } + enum "2d" { + description "2D fix (latitude/longitude only)"; + } + enum "3d" { + description "3D fix (latitude/longitude/altitude)"; + } + } + description + "Current GPS fix mode. A 3D fix is required for accurate time."; + } + + leaf latitude { + config false; + type decimal64 { + fraction-digits 6; + } + units "degrees"; + description + "Current latitude in decimal degrees. Negative values are south."; + } + + leaf longitude { + config false; + type decimal64 { + fraction-digits 6; + } + units "degrees"; + description + "Current longitude in decimal degrees. Negative values are west."; + } + + leaf altitude { + config false; + type decimal64 { + fraction-digits 1; + } + units "meters"; + description + "Current altitude above mean sea level in meters."; + } + + leaf pps-available { + config false; + type boolean; + description + "Whether this GPS device provides a PPS (Pulse Per Second) signal. + PPS provides microsecond-level timing accuracy."; + } + } } } diff --git a/src/confd/yang/confd/infix-hardware@2026-01-19.yang b/src/confd/yang/confd/infix-hardware@2026-02-08.yang similarity index 100% rename from src/confd/yang/confd/infix-hardware@2026-01-19.yang rename to src/confd/yang/confd/infix-hardware@2026-02-08.yang diff --git a/src/confd/yang/confd/infix-ntp.yang b/src/confd/yang/confd/infix-ntp.yang index d3576b85b..a3375a634 100644 --- a/src/confd/yang/confd/infix-ntp.yang +++ b/src/confd/yang/confd/infix-ntp.yang @@ -15,10 +15,30 @@ module infix-ntp { "RFC 6991: Common YANG Data Types"; } + import ietf-hardware { + prefix iehw; + reference + "RFC 8348: A YANG Data Model for Hardware Management"; + } + + import infix-hardware { + prefix ih; + } + organization "KernelKit"; contact "kernelkit@googlegroups.com"; description "Infix deviations and augments to ietf-ntp."; + revision 2026-02-08 { + description + "Add GPS/GNSS reference clock support. + + Augments refclock-master with a list of hardware GPS sources that + chronyd can use via gpsd's shared memory (SHM) interface. Supports + multiple GNSS receivers for redundancy (GPS, GLONASS, Galileo, BeiDou)."; + reference "internal"; + } + revision 2025-12-03 { description "Initial release - NTP server support. @@ -70,6 +90,149 @@ module infix-ntp { } } + /* + * GPS/GNSS reference clock sources + * + * Augments refclock-master with hardware GPS sources that chronyd + * can use via gpsd's shared memory (SHM) interface. + */ + augment "/ntp:ntp/ntp:refclock-master" { + list source { + key "receiver"; + description + "List of GPS/GNSS hardware reference clock sources. + + Each source references a GPS receiver hardware component and + configures how chronyd should use it. The hardware component + name (e.g., 'gps0') determines the SHM unit number: + - gps0 -> SHM 0 (GPS time), SHM 1 (PPS if available) + - gps1 -> SHM 2 (GPS time), SHM 3 (PPS if available) + + Multiple sources can be configured for redundancy against + jamming or hardware failure. Chronyd will evaluate all sources + and select the best one (or combine them)."; + + leaf receiver { + type leafref { + path "/iehw:hardware/iehw:component/iehw:name"; + } + must "derived-from-or-self(/iehw:hardware/iehw:component[iehw:name=current()]/iehw:class, 'ih:gps')" { + error-message "Referenced hardware component must be a GPS receiver (class 'ih:gps')"; + } + description + "Reference to a GPS receiver hardware component. + + The component name determines the device path (/dev/gpsN) + and the SHM unit number for chronyd."; + } + + leaf poll { + type int8 { + range "-6..10"; + } + default "2"; + description + "Polling interval in log2 seconds. + + Common values: + - -6 = 1/64 second (for hardware with PPS) + - 0 = 1 second + - 2 = 4 seconds (default, good for GPS without PPS) + - 4 = 16 seconds + + Lower values provide faster response but higher CPU usage."; + } + + leaf precision { + type decimal64 { + fraction-digits 9; + range "0.000000001..1.0"; + } + default "0.1"; + units "seconds"; + description + "Assumed precision of the reference clock in seconds. + + Typical values: + - 0.1 (100ms) - GPS without PPS, default + - 0.001 (1ms) - GPS with software PPS processing + - 0.000001 (1us) - GPS with hardware PPS"; + } + + leaf offset { + type decimal64 { + fraction-digits 9; + } + default "0.0"; + units "seconds"; + description + "Constant offset correction in seconds. + + Used to compensate for known systematic delays in the + GPS receiver or signal path. Positive values mean the + GPS time is ahead of true time."; + } + + leaf delay { + type decimal64 { + fraction-digits 9; + range "0.0..1.0"; + } + default "0.0"; + units "seconds"; + description + "Assumed maximum delay from the GPS receiver in seconds. + + This accounts for processing delays in gpsd and the + GPS receiver. Default is 0 (let chronyd estimate)."; + } + + leaf refid { + type string { + length "1..4"; + pattern "[A-Z0-9]+"; + } + default "GPS"; + description + "Reference identifier (refid) shown in chronyc tracking. + + Common values: + - GPS - Generic GPS + - GLO - GLONASS + - GAL - Galileo + - BDS - BeiDou + - PPS - Pulse Per Second (for PPS-only sources) + - GNSS - Multi-constellation receiver"; + } + + leaf prefer { + type boolean; + default false; + description + "Prefer this source over others when multiple are available. + + When multiple reference clocks have similar quality, the + preferred source will be selected. Useful for designating + a primary GPS receiver."; + } + + leaf pps { + type boolean; + default false; + description + "Use PPS (Pulse Per Second) signal from this source. + + When enabled, chronyd will also configure the PPS SHM + unit (2*N+1) for microsecond-level accuracy. The GPS + receiver must provide PPS and gpsd must be configured + to expose it. + + Note: PPS requires the GPS source for initial time lock, + then provides precise time edges."; + } + } + } + /* * Additional operational state fields from chronyd * that are not part of the standard ietf-ntp model diff --git a/src/confd/yang/confd/infix-ntp@2025-12-03.yang b/src/confd/yang/confd/infix-ntp@2026-02-08.yang similarity index 100% rename from src/confd/yang/confd/infix-ntp@2025-12-03.yang rename to src/confd/yang/confd/infix-ntp@2026-02-08.yang From fe9e2ce190a26981dfe9b2e4cf64a7e07f00bd11 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 8 Feb 2026 10:01:39 +0100 Subject: [PATCH 3/5] statd: support gps reference clocks in 'show ' This patch adds a background gps monitor to statd because the gpspipe program, normally used to interface with gpsd, slows down access to the operational datastore with *seconds*. This background monitor is not load bearing for how chrony accesses the gps + nmea information from gpsd, this is handled separately in SHM. Signed-off-by: Joachim Wiberg --- src/bin/show/__init__.py | 8 + src/confd/src/hardware.c | 5 + src/statd/Makefile.am | 2 +- src/statd/gpsd.c | 415 ++++++++++++++++++++++ src/statd/gpsd.h | 32 ++ src/statd/python/cli_pretty/cli_pretty.py | 81 ++++- src/statd/python/yanger/ietf_hardware.py | 80 +++++ src/statd/python/yanger/ietf_ntp.py | 42 ++- src/statd/python/yanger/ietf_system.py | 26 ++ src/statd/python/yanger/mock.leases | 3 + src/statd/statd.c | 21 +- 11 files changed, 710 insertions(+), 5 deletions(-) create mode 100644 src/statd/gpsd.c create mode 100644 src/statd/gpsd.h create mode 100644 src/statd/python/yanger/mock.leases diff --git a/src/bin/show/__init__.py b/src/bin/show/__init__.py index f86a164aa..85846731c 100755 --- a/src/bin/show/__init__.py +++ b/src/bin/show/__init__.py @@ -106,6 +106,7 @@ def ntp(args: List[str]) -> None: # Fetch both client and server operational data client_data = get_json("/system-state/ntp") server_data = get_json("/ietf-ntp:ntp") + hw_data = get_json("/ietf-hardware:hardware") # Merge into single data structure data = {} @@ -113,6 +114,8 @@ def ntp(args: List[str]) -> None: data.update(client_data) if server_data: data.update(server_data) + if hw_data: + data.update(hw_data) if RAW_OUTPUT: if not data: @@ -147,6 +150,11 @@ def ntp_source(args: List[str]) -> None: print("No ntp server data retrieved.") return + # Also fetch hardware data for GPS receiver info + hw_data = get_json("/ietf-hardware:hardware") + if hw_data: + data.update(hw_data) + if RAW_OUTPUT: print(json.dumps(data, indent=2)) return diff --git a/src/confd/src/hardware.c b/src/confd/src/hardware.c index e7c304500..5857ceb12 100644 --- a/src/confd/src/hardware.c +++ b/src/confd/src/hardware.c @@ -707,6 +707,11 @@ int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct l free(wifi_iface_list); wifi_iface_list = NULL; wifi_iface_count = 0; + } else if (!strcmp(class, "infix-hardware:gps")) { + if (event != SR_EV_DONE) + continue; + + systemf("initctl -nbq touch statd"); } } diff --git a/src/statd/Makefile.am b/src/statd/Makefile.am index 9dd4fa4d7..160495b20 100644 --- a/src/statd/Makefile.am +++ b/src/statd/Makefile.am @@ -2,7 +2,7 @@ DISTCLEANFILES = *~ *.d ACLOCAL_AMFLAGS = -I m4 sbin_PROGRAMS = statd -statd_SOURCES = statd.c shared.c shared.h journal.c journal_retention.c journal.h +statd_SOURCES = statd.c shared.c shared.h journal.c journal_retention.c journal.h gpsd.c gpsd.h statd_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE statd_CFLAGS = -W -Wall -Wextra statd_CFLAGS += $(jansson_CFLAGS) $(libyang_CFLAGS) $(sysrepo_CFLAGS) diff --git a/src/statd/gpsd.c b/src/statd/gpsd.c new file mode 100644 index 000000000..770b4dbed --- /dev/null +++ b/src/statd/gpsd.c @@ -0,0 +1,415 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +/* + * Background GPS monitor for statd. + * + * Maintains a persistent connection to gpsd (localhost:2947) and caches + * GPS device status to /run/gps-status.json. The yanger ietf_hardware + * module reads this cache instead of spawning gpspipe, avoiding blocking + * the operational datastore. + * + * Activated on SIGHUP when sysrepo running config contains a hardware + * component with class infix-hardware:gps, reconnects automatically + * if gpsd restarts. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "gpsd.h" + +static int gps_device_present(void) +{ + struct stat st; + int i; + + for (i = 0; i < 4; i++) { + char path[32]; + + snprintf(path, sizeof(path), "/dev/gps%d", i); + if (stat(path, &st) == 0) + return 1; + } + + return 0; +} + +static void cache_write(struct gpsd_ctx *ctx) +{ + char tmp[] = GPSD_CACHE_FILE ".XXXXXX"; + int fd; + + if (!ctx->cache) + return; + + fd = mkstemp(tmp); + if (fd < 0) { + ERROR("gpsd: failed to create temp file: %s", strerror(errno)); + return; + } + + if (json_dumpfd(ctx->cache, fd, JSON_INDENT(2)) < 0) { + ERROR("gpsd: failed to write cache"); + close(fd); + unlink(tmp); + return; + } + + close(fd); + if (rename(tmp, GPSD_CACHE_FILE) < 0) { + ERROR("gpsd: failed to rename cache: %s", strerror(errno)); + unlink(tmp); + } +} + +static void handle_devices(struct gpsd_ctx *ctx, json_t *msg) +{ + json_t *devices = json_object_get(msg, "devices"); + size_t i; + + if (!json_is_array(devices)) + return; + + for (i = 0; i < json_array_size(devices); i++) { + json_t *dev = json_array_get(devices, i); + const char *path = json_string_value(json_object_get(dev, "path")); + json_t *entry, *driver, *activated; + + if (!path) + continue; + + entry = json_object_get(ctx->cache, path); + if (!entry) { + entry = json_object(); + json_object_set_new(ctx->cache, path, entry); + } + + driver = json_object_get(dev, "driver"); + if (json_is_string(driver)) + json_object_set(entry, "driver", driver); + + /* activated is a timestamp string when active, absent when not */ + activated = json_object_get(dev, "activated"); + if (activated && json_is_string(activated) && + strlen(json_string_value(activated)) > 0) + json_object_set_new(entry, "activated", json_true()); + else + json_object_set_new(entry, "activated", json_false()); + } +} + +static void handle_tpv(struct gpsd_ctx *ctx, json_t *msg) +{ + const char *path = json_string_value(json_object_get(msg, "device")); + json_t *entry, *val; + + if (!path) + return; + + entry = json_object_get(ctx->cache, path); + if (!entry) { + entry = json_object(); + json_object_set_new(ctx->cache, path, entry); + } + + /* Fix mode: 0=unknown, 1=none, 2=2D, 3=3D */ + val = json_object_get(msg, "mode"); + if (json_is_integer(val)) + json_object_set(entry, "mode", val); + + val = json_object_get(msg, "lat"); + if (json_is_number(val)) + json_object_set(entry, "lat", val); + + val = json_object_get(msg, "lon"); + if (json_is_number(val)) + json_object_set(entry, "lon", val); + + val = json_object_get(msg, "altHAE"); + if (json_is_number(val)) + json_object_set(entry, "altHAE", val); +} + +static void handle_sky(struct gpsd_ctx *ctx, json_t *msg) +{ + const char *path = json_string_value(json_object_get(msg, "device")); + json_t *entry, *sats; + size_t i, visible, used; + + if (!path) + return; + + entry = json_object_get(ctx->cache, path); + if (!entry) { + entry = json_object(); + json_object_set_new(ctx->cache, path, entry); + } + + sats = json_object_get(msg, "satellites"); + if (!json_is_array(sats)) + return; + + visible = json_array_size(sats); + used = 0; + for (i = 0; i < visible; i++) { + json_t *sat = json_array_get(sats, i); + + if (json_is_true(json_object_get(sat, "used"))) + used++; + } + + json_object_set_new(entry, "satellites_visible", json_integer(visible)); + json_object_set_new(entry, "satellites_used", json_integer(used)); +} + +static void process_line(struct gpsd_ctx *ctx, const char *line) +{ + json_error_t err; + json_t *msg; + const char *cls; + + msg = json_loads(line, 0, &err); + if (!msg) + return; + + cls = json_string_value(json_object_get(msg, "class")); + if (!cls) + goto out; + + if (strcmp(cls, "DEVICES") == 0) + handle_devices(ctx, msg); + else if (strcmp(cls, "TPV") == 0) + handle_tpv(ctx, msg); + else if (strcmp(cls, "SKY") == 0) + handle_sky(ctx, msg); + + cache_write(ctx); +out: + json_decref(msg); +} + +static void gpsd_disconnect(struct gpsd_ctx *ctx) +{ + if (!ctx->connected) + return; + + ev_io_stop(ctx->loop, &ctx->sock_watcher); + close(ctx->sock_fd); + ctx->sock_fd = -1; + ctx->connected = 0; + ctx->buf_used = 0; + DEBUG("gpsd: disconnected"); +} + +static void sock_read_cb(struct ev_loop *, ev_io *w, int) +{ + struct gpsd_ctx *ctx = (struct gpsd_ctx *)w->data; + char *start, *nl; + ssize_t n; + + n = read(ctx->sock_fd, ctx->buf + ctx->buf_used, + sizeof(ctx->buf) - ctx->buf_used - 1); + if (n <= 0) { + if (n < 0 && (errno == EAGAIN || errno == EINTR)) + return; + DEBUG("gpsd: connection lost (%s)", n == 0 ? "EOF" : strerror(errno)); + gpsd_disconnect(ctx); + return; + } + + ctx->buf_used += n; + ctx->buf[ctx->buf_used] = '\0'; + + /* Process complete lines (gpsd sends one JSON object per line) */ + start = ctx->buf; + while ((nl = strchr(start, '\n')) != NULL) { + *nl = '\0'; + if (nl > start) + process_line(ctx, start); + start = nl + 1; + } + + /* Shift remaining partial line to beginning of buffer */ + if (start != ctx->buf) { + ctx->buf_used -= (start - ctx->buf); + memmove(ctx->buf, start, ctx->buf_used); + } + + /* Buffer overflow protection */ + if (ctx->buf_used >= sizeof(ctx->buf) - 1) { + ERROR("gpsd: read buffer overflow, resetting"); + ctx->buf_used = 0; + } +} + +static int gpsd_connect(struct gpsd_ctx *ctx) +{ + static const char watch_cmd[] = "?WATCH={\"enable\":true,\"json\":true};\n"; + struct sockaddr_in addr; + int fd, flags; + + fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + DEBUG("gpsd: socket(): %s", strerror(errno)); + return -1; + } + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(GPSD_PORT); + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + DEBUG("gpsd: connect(): %s", strerror(errno)); + close(fd); + return -1; + } + + /* Enable JSON watch mode (socket still blocking, localhost = instant) */ + if (write(fd, watch_cmd, strlen(watch_cmd)) < 0) { + ERROR("gpsd: failed to send WATCH command: %s", strerror(errno)); + close(fd); + return -1; + } + + /* Switch to non-blocking for ev_io */ + flags = fcntl(fd, F_GETFL, 0); + if (flags >= 0) + fcntl(fd, F_SETFL, flags | O_NONBLOCK); + + /* Clear stale cache data from previous connection */ + json_object_clear(ctx->cache); + + ctx->sock_fd = fd; + ctx->connected = 1; + ctx->buf_used = 0; + + ev_io_init(&ctx->sock_watcher, sock_read_cb, fd, EV_READ); + ctx->sock_watcher.data = ctx; + ev_io_start(ctx->loop, &ctx->sock_watcher); + + INFO("gpsd: connected"); + return 0; +} + +static void check_timer_cb(struct ev_loop *, ev_timer *w, int) +{ + struct gpsd_ctx *ctx = (struct gpsd_ctx *)w->data; + + if (ctx->connected) + return; + + if (!gps_device_present()) { + unlink(GPSD_CACHE_FILE); + return; + } + + gpsd_connect(ctx); +} + +/* + * Check sysrepo running config for hardware components with class + * infix-hardware:gps. Returns 1 if at least one is found. + */ +static int has_gps_component(sr_conn_ctx_t *conn) +{ + sr_session_ctx_t *ses; + sr_val_t *vals = NULL; + size_t cnt = 0; + int found; + + if (sr_session_start(conn, SR_DS_RUNNING, &ses)) + return 0; + + sr_get_items(ses, + "/ietf-hardware:hardware/component[class='infix-hardware:gps']/name", + 0, 0, &vals, &cnt); + + found = cnt > 0; + sr_free_values(vals, cnt); + sr_session_stop(ses); + + return found; +} + +static void gpsd_activate(struct gpsd_ctx *ctx) +{ + if (ctx->active) + return; + + ctx->active = 1; + ev_timer_start(ctx->loop, &ctx->check_timer); + + if (gps_device_present()) + gpsd_connect(ctx); + + INFO("gpsd: GPS monitoring activated"); +} + +static void gpsd_deactivate(struct gpsd_ctx *ctx) +{ + if (!ctx->active) + return; + + ctx->active = 0; + gpsd_disconnect(ctx); + ev_timer_stop(ctx->loop, &ctx->check_timer); + json_object_clear(ctx->cache); + unlink(GPSD_CACHE_FILE); + + INFO("gpsd: GPS monitoring deactivated"); +} + +void gpsd_reload(struct gpsd_ctx *ctx) +{ + if (has_gps_component(ctx->sr_conn)) + gpsd_activate(ctx); + else + gpsd_deactivate(ctx); +} + +int gpsd_init(struct gpsd_ctx *ctx, struct ev_loop *loop, sr_conn_ctx_t *conn) +{ + memset(ctx, 0, sizeof(*ctx)); + ctx->loop = loop; + ctx->sock_fd = -1; + ctx->sr_conn = conn; + + ctx->cache = json_object(); + if (!ctx->cache) { + ERROR("gpsd: failed to create cache object"); + return -1; + } + + ev_timer_init(&ctx->check_timer, check_timer_cb, + GPSD_CHECK_INTERVAL, GPSD_CHECK_INTERVAL); + ctx->check_timer.data = ctx; + /* Timer not started here -- gpsd_reload() activates when needed */ + + INFO("gpsd: GPS monitor initialized"); + return 0; +} + +void gpsd_exit(struct gpsd_ctx *ctx) +{ + gpsd_deactivate(ctx); + + if (ctx->cache) { + json_decref(ctx->cache); + ctx->cache = NULL; + } + + INFO("gpsd: GPS monitor stopped"); +} diff --git a/src/statd/gpsd.h b/src/statd/gpsd.h new file mode 100644 index 000000000..94fdc71be --- /dev/null +++ b/src/statd/gpsd.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#ifndef STATD_GPSD_H_ +#define STATD_GPSD_H_ + +#include +#include +#include + +#define GPSD_CACHE_FILE "/run/gps-status.json" +#define GPSD_PORT 2947 +#define GPSD_CHECK_INTERVAL 10.0 /* Seconds between device/connection checks */ +#define GPSD_READ_BUFSZ 4096 + +struct gpsd_ctx { + struct ev_loop *loop; + ev_timer check_timer; /* Periodic check for GPS devices / reconnect */ + ev_io sock_watcher; /* Read watcher on gpsd socket */ + int sock_fd; /* TCP socket to gpsd */ + char buf[GPSD_READ_BUFSZ]; /* Line accumulation buffer */ + size_t buf_used; + json_t *cache; /* Accumulated GPS data, keyed by device path */ + int connected; + sr_conn_ctx_t *sr_conn; /* Sysrepo connection for config queries */ + int active; /* GPS monitoring enabled (config has gps component) */ +}; + +int gpsd_init(struct gpsd_ctx *ctx, struct ev_loop *loop, sr_conn_ctx_t *conn); +void gpsd_reload(struct gpsd_ctx *ctx); +void gpsd_exit(struct gpsd_ctx *ctx); + +#endif diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py index 488655d7a..54bc5e051 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -2132,6 +2132,7 @@ def show_hardware(json): usb_ports = [c for c in components if c.get("class") == "infix-hardware:usb"] sensors = [c for c in components if c.get("class") == "iana-hardware:sensor"] wifi_radios = [c for c in components if c.get("class") == "infix-hardware:wifi"] + gps_receivers = [c for c in components if c.get("class") == "infix-hardware:gps"] width = max(PadSensor.table_width(), 62) @@ -2203,6 +2204,44 @@ def show_hardware(json): radios_table.row(phy, manufacturer, bands_str, standard_str, max_ap) radios_table.print() + if gps_receivers: + Decore.title("GPS/GNSS Receivers", width) + + for component in gps_receivers: + gps = component.get("infix-hardware:gps-receiver", {}) + name = component.get("name", "unknown") + device = gps.get("device", "N/A") + driver = gps.get("driver", "Unknown") + fix = gps.get("fix-mode", "none") + activated = gps.get("activated", False) + + print(f"{'Name':<20}: {name}") + print(f"{'Device':<20}: {device}") + print(f"{'Driver':<20}: {driver}") + print(f"{'Status':<20}: {'Active' if activated else 'Inactive'}") + print(f"{'Fix':<20}: {fix.upper()}") + + sat_vis = gps.get("satellites-visible") + sat_used = gps.get("satellites-used") + if sat_vis is not None: + print(f"{'Satellites':<20}: {sat_used}/{sat_vis} (used/visible)") + + lat = gps.get("latitude") + lon = gps.get("longitude") + alt = gps.get("altitude") + if lat is not None and lon is not None: + lat_f = float(lat) + lon_f = float(lon) + lat_dir = "N" if lat_f >= 0 else "S" + lon_dir = "E" if lon_f >= 0 else "W" + pos = f"{abs(lat_f):.6f}{lat_dir} {abs(lon_f):.6f}{lon_dir}" + if alt is not None: + pos += f" {alt}m" + print(f"{'Position':<20}: {pos}") + + pps = gps.get("pps-available", False) + print(f"{'PPS':<20}: {'Available' if pps else 'Not available'}") + if usb_ports: Decore.title("USB Ports", width) @@ -2509,9 +2548,16 @@ def show_ntp(json, address=None): show_ntp_source_detail_single(matching[0], False) return + # Check for GPS/GNSS hardware reference clocks + hw_components = json.get("ietf-hardware:hardware", {}).get("component", []) + gps_sources = [c for c in hw_components if c.get("class") == "infix-hardware:gps"] + if is_server: if sources: print(f"{'Mode':<20}: Relay (no local reference clock)") + elif gps_sources: + gps_names = ", ".join(c.get("name", "?") for c in gps_sources) + print(f"{'Mode':<20}: Server (GPS reference clock: {gps_names})") else: print(f"{'Mode':<20}: Server (local reference clock)") print(f"{'Port':<20}: {port}") @@ -2808,10 +2854,43 @@ def show_ntp_source(json, address=None): return associations = ntp_data.get("associations", {}).get("association", []) - if not associations: + + # Check for GPS/GNSS reference clock sources from hardware data + hw_components = json.get("ietf-hardware:hardware", {}).get("component", []) + gps_sources = [c for c in hw_components if c.get("class") == "infix-hardware:gps"] + + # Show GPS reference clocks + if gps_sources: + clock_state = ntp_data.get("clock-state", {}).get("system-status", {}) + clock_refid = clock_state.get("clock-refid", "").strip() + + for gps in gps_sources: + gps_data = gps.get("infix-hardware:gps-receiver", {}) + name = gps.get("name", "unknown") + driver = gps_data.get("driver", "Unknown") + fix = gps_data.get("fix-mode", "none") + activated = gps_data.get("activated", False) + sat_used = gps_data.get("satellites-used", 0) + sat_vis = gps_data.get("satellites-visible", 0) + + # Determine if this GPS is the current sync source + is_synced = clock_refid in ("GPS", "PPS", "GLO", "GAL", "BDS", "GNSS") + + state = "selected" if is_synced else ("active" if activated else "inactive") + print(f"{'Reference Clock':<20}: {name} ({driver})") + print(f"{'Status':<20}: {state}") + print(f"{'Fix Mode':<20}: {fix.upper()}") + if sat_vis: + print(f"{'Satellites':<20}: {sat_used}/{sat_vis} (used/visible)") + print() + + if not associations and not gps_sources: print("No NTP associations found.") return + if not associations: + return + # If address specified, show detailed view for that association if address: matching = [a for a in associations if a.get('address') == address] diff --git a/src/statd/python/yanger/ietf_hardware.py b/src/statd/python/yanger/ietf_hardware.py index 4ee32970b..b68493c4e 100644 --- a/src/statd/python/yanger/ietf_hardware.py +++ b/src/statd/python/yanger/ietf_hardware.py @@ -667,6 +667,85 @@ def wifi_radio_components(): return components +def gps_receiver_components(): + """Discover GPS/GNSS receivers and populate operational state. + + GPS devices are discovered via /dev/gps* symlinks (created by udev rules). + Status is read from /run/gps-status.json, a cache maintained by statd's + background GPS monitor (gpsd.c) which streams data from gpsd without + blocking the operational datastore. + """ + components = [] + + # Discover GPS devices via /dev/gps* symlinks (created by udev rules) + gps_devices = {} + for i in range(4): + dev_path = f"/dev/gps{i}" + if not HOST.exists(dev_path): + continue + # Resolve symlink to actual device (for matching gpsd cache keys) + actual = HOST.run(("readlink", "-f", dev_path), default="").strip() + gps_devices[actual] = { + "name": f"gps{i}", + "symlink": dev_path, + } + + if not gps_devices: + return components + + # Read cached GPS status from statd background monitor + cache = HOST.read_json("/run/gps-status.json", {}) + + # Build hardware components for each discovered GPS device + for actual_path, dev in gps_devices.items(): + name = dev["name"] + component = { + "name": name, + "class": "infix-hardware:gps", + "description": "GPS/GNSS Receiver" + } + + gps_data = {} + gps_data["device"] = dev["symlink"] + + # Look up cached status by actual device path + info = cache.get(actual_path, {}) + + if info.get("driver"): + gps_data["driver"] = info["driver"] + gps_data["activated"] = bool(info.get("activated")) + + mode = info.get("mode", 0) + if mode == 2: + gps_data["fix-mode"] = "2d" + elif mode == 3: + gps_data["fix-mode"] = "3d" + else: + gps_data["fix-mode"] = "none" + + if "lat" in info: + gps_data["latitude"] = f"{float(info['lat']):.6f}" + if "lon" in info: + gps_data["longitude"] = f"{float(info['lon']):.6f}" + if "altHAE" in info: + gps_data["altitude"] = f"{float(info['altHAE']):.1f}" + + if "satellites_visible" in info: + gps_data["satellites-visible"] = int(info["satellites_visible"]) + gps_data["satellites-used"] = int(info.get("satellites_used", 0)) + + # Check for PPS device availability + pps_path = f"/dev/pps{name.replace('gps', '')}" + gps_data["pps-available"] = HOST.exists(pps_path) + + if gps_data: + component["infix-hardware:gps-receiver"] = gps_data + + components.append(component) + + return components + + def operational(): systemjson = HOST.read_json("/run/system.json", {}) @@ -679,6 +758,7 @@ def operational(): hwmon_sensor_components() + thermal_sensor_components() + wifi_radio_components() + + gps_receiver_components() + [], }, } diff --git a/src/statd/python/yanger/ietf_ntp.py b/src/statd/python/yanger/ietf_ntp.py index cdfade9e2..80c80e2e6 100644 --- a/src/statd/python/yanger/ietf_ntp.py +++ b/src/statd/python/yanger/ietf_ntp.py @@ -5,7 +5,39 @@ def add_ntp_associations(out): - """Add NTP association information from chronyc sources and sourcestats""" + """Add NTP association information from chronyc sources and sourcestats. + + The chronyc -c sources output is CSV with the following fields: + [0] Mode indicator: + ^ server (we're a client to this source) + = peer (symmetric mode) + # local reference clock (GPS, PPS, etc.) - skipped, no IP address + [1] State indicator: + * selected (current sync source) + + candidate + - outlier + ? unusable + x falseticker + ~ unstable + [2] Address (IP address or refclock name like "GPS") + [3] Stratum + [4] Poll interval (log2 seconds) + [5] Reach (octal reachability register) + [6] LastRx (seconds since last response) + [7] Last offset (seconds) + [8] Offset at last update (seconds) + [9] Error estimate (seconds) + + The chronyc -c sourcestats output is CSV with: + [0] Address + [1] NP (number of sample points) + [2] NR (number of runs) + [3] Span (seconds) + [4] Frequency (ppm) + [5] Freq Skew (ppm) + [6] Offset (seconds) + [7] Std Dev (seconds) + """ try: # Get basic source information sources_data = HOST.run_multiline(["chronyc", "-c", "sources"], []) @@ -44,6 +76,10 @@ def add_ntp_associations(out): continue mode_indicator = parts[0] + # Skip reference clocks (mode "#") as they have names like "GPS" instead of IP addresses + if mode_indicator == "#": + continue + state_indicator = parts[1] address = parts[2] stratum = int(parts[3]) @@ -156,7 +192,9 @@ def add_ntp_clock_state(out): refid_name = parts[1] if refid_name: - system_status["clock-refid"] = refid_name + # NTP refids are always 4 bytes; chronyc strips trailing padding. + # YANG typedef 'refid' requires exactly length 4 for strings. + system_status["clock-refid"] = refid_name.ljust(4)[:4] elif refid_ip and len(refid_ip) == 8: try: a = int(refid_ip[0:2], 16) diff --git a/src/statd/python/yanger/ietf_system.py b/src/statd/python/yanger/ietf_system.py index eeca4b714..60c7c6c01 100644 --- a/src/statd/python/yanger/ietf_system.py +++ b/src/statd/python/yanger/ietf_system.py @@ -36,6 +36,29 @@ def get_boot_order(): return order def add_ntp(out): + """Add NTP source information from chronyc sources. + + The chronyc -c sources output is CSV with the following fields: + [0] Mode indicator: + ^ server (we're a client to this source) + = peer (symmetric mode) + # local reference clock (GPS, PPS, etc.) - skipped, no IP address + [1] State indicator: + * selected (current sync source) + + candidate + - outlier + ? unusable + x falseticker + ~ unstable + [2] Address (IP address or refclock name like "GPS") + [3] Stratum + [4] Poll interval (log2 seconds) + [5] Reach (octal reachability register) + [6] LastRx (seconds since last response) + [7] Last offset (seconds) + [8] Offset at last update (seconds) + [9] Error estimate (seconds) + """ data = HOST.run_multiline(["chronyc", "-c", "sources"], []) source = [] state_mode_map = { @@ -54,6 +77,9 @@ def add_ntp(out): for line in data: src = {} line = line.split(',') + # Skip reference clocks (mode "#") as they have names like "GPS" instead of IP addresses + if line[0] == "#": + continue src["address"] = line[2] src["mode"] = state_mode_map[line[0]] src["state"] = source_state_map[line[1]] diff --git a/src/statd/python/yanger/mock.leases b/src/statd/python/yanger/mock.leases new file mode 100644 index 000000000..b27e301e0 --- /dev/null +++ b/src/statd/python/yanger/mock.leases @@ -0,0 +1,3 @@ +1692853200 00:1a:2b:3c:4d:5e 192.168.1.100 hostname01 01:00:1a:2b:3c:4d:5e +1692853800 00:1a:2b:3c:4d:5f 192.168.1.101 hostname02 * +1692854400 00:1a:2b:3c:4d:60 192.168.1.102 hostname03 01:00:1a:2b:3c:4d:60 diff --git a/src/statd/statd.c b/src/statd/statd.c index b07ce04c1..139c09435 100644 --- a/src/statd/statd.c +++ b/src/statd/statd.c @@ -30,6 +30,7 @@ #include "shared.h" #include "journal.h" +#include "gpsd.h" /* New kernel feature, not in sys/mman.h yet */ #ifndef MFD_NOEXEC_SEAL @@ -69,6 +70,7 @@ struct statd { sr_conn_ctx_t *sr_conn; /* Connection (owns YANG context) */ struct ev_loop *ev_loop; struct journal_ctx journal; /* Journal thread context */ + struct gpsd_ctx gpsd; /* GPS monitor context */ }; static int ly_add_yanger_data(const struct ly_ctx *ctx, struct lyd_node **parent, @@ -350,6 +352,14 @@ static void sigusr1_cb(struct ev_loop *, struct ev_signal *, int) debug ^= 1; } +static void sighup_cb(struct ev_loop *, struct ev_signal *w, int) +{ + struct statd *statd = (struct statd *)w->data; + + INFO("SIGHUP received, reloading GPS config"); + gpsd_reload(&statd->gpsd); +} + static void sr_event_cb(struct ev_loop *, struct ev_io *w, int) { struct sub *sub = (struct sub *)w->data; @@ -452,7 +462,7 @@ static int subscribe_to_all(struct statd *statd) int main(int argc, char *argv[]) { - struct ev_signal sigint_watcher, sigusr1_watcher; + struct ev_signal sigint_watcher, sigusr1_watcher, sighup_watcher; int log_opts = LOG_PID | LOG_NDELAY; struct statd statd = {}; const char *env; @@ -513,6 +523,10 @@ int main(int argc, char *argv[]) sigusr1_watcher.data = &statd; ev_signal_start(statd.ev_loop, &sigusr1_watcher); + ev_signal_init(&sighup_watcher, sighup_cb, SIGHUP); + sighup_watcher.data = &statd; + ev_signal_start(statd.ev_loop, &sighup_watcher); + err = journal_start(&statd.journal, statd.sr_query_ses); if (err) { sr_session_stop(statd.sr_query_ses); @@ -521,6 +535,10 @@ int main(int argc, char *argv[]) return EXIT_FAILURE; } + if (gpsd_init(&statd.gpsd, statd.ev_loop, statd.sr_conn)) + INFO("GPS monitoring not available"); + gpsd_reload(&statd.gpsd); + /* Signal readiness to Finit */ pidfile(NULL); @@ -530,6 +548,7 @@ int main(int argc, char *argv[]) /* We should never get here during normal operation */ INFO("Status daemon shutting down"); + gpsd_exit(&statd.gpsd); journal_stop(&statd.journal); unsub_to_all(&statd); From af6d677d70a2097408cd2ba8e17b0f2978db8622 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 8 Feb 2026 20:00:14 +0100 Subject: [PATCH 4/5] doc: add section on GPS receivers and GPS reference clock Signed-off-by: Joachim Wiberg --- doc/hardware.md | 50 ++++++++++++++++++++++++++++++++++++++ doc/ntp.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/doc/hardware.md b/doc/hardware.md index 814fec296..f794e20eb 100644 --- a/doc/hardware.md +++ b/doc/hardware.md @@ -3,6 +3,56 @@ The hardware infomation and status is handled by the YANG model [IETF hardware][1], with deviations and augmentations in _infix-hardware_. +## GPS/GNSS Receivers + +Infix supports GPS/GNSS receivers for hardware status monitoring and NTP +time synchronization. USB GPS receivers using the USB ACM interface are +supported (e.g., u-blox). When connected, devices are automatically +discovered and named `gps0`, `gps1`, etc. + +### Current status + +
admin@example:/> show hardware
+HARDWARE COMPONENTS                                           
+──────────────────────────────────────────────────────────────
+GPS/GNSS Receivers                                           
+Name                : gps0
+Device              : /dev/gps0
+Driver              : u-blox
+Status              : Active
+Fix                 : 3D
+Satellites          : 10/15 (used/visible)
+Position            : 59.334567N 18.063456E 42.3m
+PPS                 : Available
+
+ +Available information per receiver: + +| Field | Description | +|------------|---------------------------------------------------| +| Name | Component name (`gps0`, `gps1`, ...) | +| Device | Device path (`/dev/gps0`) | +| Driver | Protocol driver (e.g., `u-blox`, `NMEA`, `SiRF`) | +| Status | `Active` or `Inactive` | +| Fix | `NONE`, `2D`, or `3D` | +| Satellites | Used/visible count | +| Position | Latitude, longitude, altitude (when fix acquired) | +| PPS | Pulse Per Second signal availability | + +### Configure GPS receiver + +GPS receivers are hardware components with class `gps`. The class is +auto-inferred from the component name, similar to WiFi radios (`radioN`): + +
admin@example:/> configure
+admin@example:/config/> set hardware component gps0
+admin@example:/config/> leave
+
+ +To use a GPS receiver as an NTP reference clock source, see +[NTP — GPS Reference Clock](ntp.md#gps-reference-clock). + + ## USB Ports For Infix to be able to control USB port(s), a device tree modification diff --git a/doc/ntp.md b/doc/ntp.md index 67ea2bbc8..100e955e6 100644 --- a/doc/ntp.md +++ b/doc/ntp.md @@ -28,6 +28,70 @@ admin@example:/config/ntp/> set refclock-master master-stratum 10 admin@example:/config/ntp/> leave +## GPS Reference Clock + +A GPS/GNSS receiver can be used as an NTP reference clock source, +providing stratum 1 time derived from the GPS satellite constellation. +This requires a GPS hardware component to be configured first, see +[Hardware — GPS/GNSS Receivers](hardware.md#gpsgnss-receivers). + +### Basic setup + +Add a GPS receiver as a reference clock source: + +
admin@example:/config/> edit ntp
+admin@example:/config/ntp/> edit refclock-master source gps0
+admin@example:/config/ntp/refclock-master/source/gps0/> set poll 2
+admin@example:/config/ntp/refclock-master/source/gps0/> set precision 0.1
+admin@example:/config/ntp/refclock-master/source/gps0/> end
+admin@example:/config/ntp/> leave
+
+ +Tunable parameters: + +| Parameter | Default | Description | +|-------------|--------:|----------------------------------------------------| +| `poll` | `2` | Polling interval in log2 seconds (2 = 4s) | +| `precision` | `0.1` | Assumed precision in seconds (0.1 = 100ms) | +| `refid` | `"GPS"`| Reference identifier (e.g., `GPS`, `GNSS`, `GLO`) | +| `prefer` | `false` | Prefer this source over other reference clocks | +| `pps` | `false` | Enable PPS for microsecond-level accuracy | +| `offset` | `0.0` | Constant offset correction in seconds | +| `delay` | `0.0` | Assumed maximum delay from the receiver | + +### PPS (Pulse Per Second) + +When the GPS receiver provides a PPS signal, enable the `pps` option for +microsecond-level accuracy. With PPS, the GPS time provides the initial +lock and the PPS edges discipline the clock: + +
admin@example:/config/ntp/> edit refclock-master source gps0
+admin@example:/config/ntp/refclock-master/source/gps0/> set pps true
+admin@example:/config/ntp/refclock-master/source/gps0/> set precision 0.000001
+admin@example:/config/ntp/refclock-master/source/gps0/> end
+admin@example:/config/ntp/> leave
+
+ +### Monitoring + +The `show ntp` command shows the GPS receiver as the reference clock source: + +
admin@example:/> show ntp
+Mode                : Server (GPS reference clock: gps0)
+Port                : 123
+Stratum             : 1
+Ref time (UTC)      : Sun Feb 08 19:44:36 2026
+
+ +Use `show ntp source` to see GPS reference clock details: + +
admin@example:/> show ntp source
+Reference Clock     : gps0 (u-blox)
+Status              : selected
+Fix Mode            : 3D
+Satellites          : 9/17 (used/visible)
+
+ ## Server Mode Synchronize from upstream NTP servers while serving time to clients: From 0880f694770e396327accccb1d3b6eba7702226d Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 8 Feb 2026 20:00:45 +0100 Subject: [PATCH 5/5] doc: update ChangeLog, initial GPS receiver support Signed-off-by: Joachim Wiberg --- doc/ChangeLog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index bdfbab2f9..7cc4529b8 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -10,6 +10,7 @@ All notable changes to the project are documented in this file. - Upgrade Linux kernel to 6.18.9 (LTS) - Add support for Microchip SAMA7G54-EK Evaluation Kit, Arm Cortex-A7 +- Add GPS/GNSS receiver support with NTP reference clock integration ### Fixes