Files
2026-04-24 14:29:58 +02:00

143 lines
4.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""PTP (IEEE 1588) test helpers
Query PTP operational data from the ieee1588-ptp-tt YANG model.
All functions are None-safe and intended for use with until():
until(lambda: ptp.is_time_receiver(target), attempts=60)
"""
def _get_instance(target, idx=0):
data = target.get_data("/ieee1588-ptp-tt:ptp") or {}
instances = (data.get("ptp", {})
.get("instances", {})
.get("instance", []))
for inst in instances:
if inst.get("instance-index") == idx:
return inst
return None
def port_state(target, port_idx=1, inst_idx=0):
"""Return port-state string for given port, or None."""
inst = _get_instance(target, inst_idx)
if not inst:
return None
for port in inst.get("ports", {}).get("port", []):
if port.get("port-index") == port_idx:
return port.get("port-ds", {}).get("port-state")
return None
def is_time_receiver(target, port_idx=1, inst_idx=0):
"""True when port is in time-receiver state."""
return port_state(target, port_idx, inst_idx) == "time-receiver"
def is_time_transmitter(target, port_idx=1, inst_idx=0):
"""True when port is in time-transmitter state."""
return port_state(target, port_idx, inst_idx) == "time-transmitter"
def offset_ns(target, inst_idx=0):
"""Return offset-from-time-transmitter in nanoseconds, or None.
The YANG value is scaled nanoseconds (int64 × 2^16 stored as string).
"""
inst = _get_instance(target, inst_idx)
if not inst:
return None
raw = inst.get("current-ds", {}).get("offset-from-time-transmitter")
try:
return int(raw) // 65536
except (TypeError, ValueError):
return None
def steps_removed(target, inst_idx=0):
"""Return steps-removed count, or None."""
inst = _get_instance(target, inst_idx)
return inst.get("current-ds", {}).get("steps-removed") if inst else None
def grandmaster_identity(target, inst_idx=0):
"""Return grandmaster-identity string from parent-ds, or None."""
inst = _get_instance(target, inst_idx)
return inst.get("parent-ds", {}).get("grandmaster-identity") if inst else None
def clock_identity(target, inst_idx=0):
"""Return this device's clock-identity string from default-ds, or None."""
inst = _get_instance(target, inst_idx)
return inst.get("default-ds", {}).get("clock-identity") if inst else None
def is_own_gm(target, inst_idx=0):
"""True when device is its own grandmaster (acting as GM).
Compares clock-identity to grandmaster-identity; equal means the
device won the BTCA election and is distributing its own time.
"""
cid = clock_identity(target, inst_idx)
gm = grandmaster_identity(target, inst_idx)
return cid is not None and cid == gm
def has_converged(target, threshold_ns=100_000, inst_idx=0):
"""True when |offset-from-time-transmitter| < threshold_ns."""
off = offset_ns(target, inst_idx)
if off is None:
return False
return abs(off) < threshold_ns
def port_state_dbg(target, port_idx=1, inst_idx=0):
"""Return a diagnostic string with instance/port state, or an error hint.
Useful in until() lambdas and test step output to show what is actually
being observed when a state check does not converge::
until(lambda: is_time_transmitter(gm) or not print(port_state_dbg(gm)),
attempts=60)
"""
data = target.get_data("/ieee1588-ptp-tt:ptp") or {}
if not data:
return f"{target.name}: no PTP operational data (ptp4l not running?)"
instances = (data.get("ptp", {})
.get("instances", {})
.get("instance", []))
if not instances:
return f"{target.name}: PTP data present but no instances"
parts = []
for inst in instances:
idx = inst.get("instance-index", "?")
for port in inst.get("ports", {}).get("port", []):
pidx = port.get("port-index", "?")
state = port.get("port-ds", {}).get("port-state", "?")
parts.append(f"inst={idx} port={pidx} state={state}")
return f"{target.name}: " + (", ".join(parts) if parts else "no ports")
def default_threshold(env, logical_node, hops=1):
"""Return a convergence threshold suited to the node's timestamping capability.
Queries the physical topology for ptp-hwts on any link connected to the
physical node matched to logical_node. Returns 1000 ns (1 µs) per hop for
hardware-timestamping nodes or 100000 ns (100 µs) for software timestamping.
Use hops=2 for a receiver behind a Boundary Clock — each BC hop adds
phc2sys relay jitter on multi-chip hardware.
Pass --threshold-ns on the command line to override.
"""
phys = env.ltop.xlate(logical_node)
g = env.ptop.g
has_hwts = any(
"ptp-hwts" in data.get("provides", set())
for _, _, data in g.edges(phys, data=True)
)
return 1_000 * hops if has_hwts else 100_000