mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-30 20:43:02 +02:00
statd: support gps reference clocks in 'show <hardware|ntp>'
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 <troglobit@gmail.com>
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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() +
|
||||
[],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user