Merge pull request #1399 from kernelkit/gps

This commit is contained in:
Joachim Wiberg
2026-02-10 21:04:34 +01:00
committed by GitHub
32 changed files with 1361 additions and 9 deletions
+4
View File
@@ -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
+4
View File
@@ -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
+4
View File
@@ -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
+4
View File
@@ -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
+4
View File
@@ -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
+4
View File
@@ -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
+4
View File
@@ -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
+1
View File
@@ -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
+50
View File
@@ -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
<pre class="cli"><code>admin@example:/> <b>show hardware</b>
<span class="header">HARDWARE COMPONENTS </span>
──────────────────────────────────────────────────────────────
<span class="title">GPS/GNSS Receivers </span>
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
</code></pre>
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`):
<pre class="cli"><code>admin@example:/> <b>configure</b>
admin@example:/config/> <b>set hardware component gps0</b>
admin@example:/config/> <b>leave</b>
</code></pre>
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
+64
View File
@@ -28,6 +28,70 @@ admin@example:/config/ntp/> <b>set refclock-master master-stratum 10</b>
admin@example:/config/ntp/> <b>leave</b>
</code></pre>
## 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:
<pre class="cli"><code>admin@example:/config/> <b>edit ntp</b>
admin@example:/config/ntp/> <b>edit refclock-master source gps0</b>
admin@example:/config/ntp/refclock-master/source/gps0/> <b>set poll 2</b>
admin@example:/config/ntp/refclock-master/source/gps0/> <b>set precision 0.1</b>
admin@example:/config/ntp/refclock-master/source/gps0/> <b>end</b>
admin@example:/config/ntp/> <b>leave</b>
</code></pre>
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:
<pre class="cli"><code>admin@example:/config/ntp/> <b>edit refclock-master source gps0</b>
admin@example:/config/ntp/refclock-master/source/gps0/> <b>set pps true</b>
admin@example:/config/ntp/refclock-master/source/gps0/> <b>set precision 0.000001</b>
admin@example:/config/ntp/refclock-master/source/gps0/> <b>end</b>
admin@example:/config/ntp/> <b>leave</b>
</code></pre>
### Monitoring
The `show ntp` command shows the GPS receiver as the reference clock source:
<pre class="cli"><code>admin@example:/> <b>show ntp</b>
Mode : Server (GPS reference clock: gps0)
Port : 123
Stratum : 1
Ref time (UTC) : Sun Feb 08 19:44:36 2026
</code></pre>
Use `show ntp source` to see GPS reference clock details:
<pre class="cli"><code>admin@example:/> <b>show ntp source</b>
Reference Clock : gps0 (u-blox)
Status : selected
Fix Mode : 3D
Satellites : 9/17 (used/visible)
</code></pre>
## Server Mode
Synchronize from upstream NTP servers while serving time to clients:
+1
View File
@@ -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"
+65
View File
@@ -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"
+34
View File
@@ -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.
+21
View File
@@ -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))
+1
View File
@@ -0,0 +1 @@
GPSD_OPTIONS="-n"
+8
View File
@@ -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
+11
View File
@@ -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:
@@ -701,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");
}
}
+75 -2
View File
@@ -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;
/* Only configure local clock if stratum is valid (1-15) */
/*
* 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");
/* 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);
+2 -2
View File
@@ -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"
)
+139
View File
@@ -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.";
}
}
}
}
+164
View File
@@ -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,150 @@ 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";
max-elements 2;
description
"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)
Two sources can be configured for redundancy against jamming
or hardware failure. The NTP server will evaluate all sources
and select the best one.";
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
+1 -1
View File
@@ -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)
+415
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <ev.h>
#include <jansson.h>
#include <srx/common.h>
#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");
}
+32
View File
@@ -0,0 +1,32 @@
/* SPDX-License-Identifier: BSD-3-Clause */
#ifndef STATD_GPSD_H_
#define STATD_GPSD_H_
#include <ev.h>
#include <jansson.h>
#include <sysrepo.h>
#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
+80 -1
View File
@@ -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]
+80
View File
@@ -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() +
[],
},
}
+40 -2
View File
@@ -5,7 +5,39 @@ from .host import HOST
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)
+26
View File
@@ -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]]
+3
View File
@@ -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
+20 -1
View File
@@ -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);