+55
-157
@@ -1,175 +1,73 @@
|
|||||||
import re
|
#!/bin/bash
|
||||||
import datetime as dt
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import matplotlib.dates as mdates
|
|
||||||
from matplotlib.lines import Line2D
|
|
||||||
|
|
||||||
import tkinter as tk
|
#### Tested running on a 1232
|
||||||
from tkinter import filedialog, messagebox, scrolledtext
|
|
||||||
|
|
||||||
|
SOCK="/var/run/ptp4l_media1"
|
||||||
|
HOST=$(hostname)
|
||||||
|
LOGFILE="ptp_media1.log"
|
||||||
|
WINDOW=60
|
||||||
|
|
||||||
def parse_log(file_path):
|
offsets=()
|
||||||
t, rms, freq = [], [], []
|
delays=()
|
||||||
state_events = []
|
freqs=()
|
||||||
master_events = []
|
|
||||||
|
|
||||||
last_timestamp = None
|
prev_offset=""
|
||||||
|
prev_time=""
|
||||||
|
|
||||||
with open(file_path, encoding='utf-8', errors='ignore') as f:
|
while true; do
|
||||||
for line in f:
|
now_fmt=$(date +"%b %d %T")
|
||||||
|
uptime=$(awk '{print $1}' /proc/uptime)
|
||||||
|
now_ns=$(date +%s%N)
|
||||||
|
|
||||||
ts_match = re.match(r'^(\w+ \d+ \d+:\d+:\d+)', line)
|
# --- get offset ---
|
||||||
if ts_match:
|
ts_np=$(pmc -u -s $SOCK "GET TIME_STATUS_NP")
|
||||||
try:
|
offset=$(echo "$ts_np" | awk '/master_offset/ {print $2}')
|
||||||
timestr = ts_match.group(1) + " 2026"
|
|
||||||
last_timestamp = dt.datetime.strptime(timestr, "%b %d %H:%M:%S %Y")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
timestamp = last_timestamp
|
# --- get delay ---
|
||||||
|
cds=$(pmc -u -s $SOCK "GET CURRENT_DATA_SET")
|
||||||
|
delay=$(echo "$cds" | awk '/meanPathDelay/ {print $2}')
|
||||||
|
|
||||||
if not timestamp:
|
# --- compute freq (ns/sec ≈ ppb-like behaviour) ---
|
||||||
continue
|
if [[ -n "$prev_offset" ]]; then
|
||||||
|
dt=$((now_ns - prev_time))
|
||||||
|
doffset=$((offset - prev_offset))
|
||||||
|
freq=$(awk -v d="$doffset" -v t="$dt" 'BEGIN {printf "%.0f", (d / t) * 1e9}')
|
||||||
|
else
|
||||||
|
freq=0
|
||||||
|
fi
|
||||||
|
|
||||||
m = re.search(r'rms\s+(\d+).*freq\s+([+-]?\d+)', line)
|
prev_offset=$offset
|
||||||
if m:
|
prev_time=$now_ns
|
||||||
t.append(timestamp)
|
|
||||||
rms.append(int(m.group(1)))
|
|
||||||
freq.append(int(m.group(2)))
|
|
||||||
|
|
||||||
m = re.search(r'port \d+: (\w+) to (\w+)', line)
|
# store
|
||||||
if m:
|
offsets+=("$offset")
|
||||||
state_events.append((timestamp, f"{m.group(1)}→{m.group(2)}"))
|
delays+=("$delay")
|
||||||
|
freqs+=("$freq")
|
||||||
|
|
||||||
m = re.search(
|
# trim window
|
||||||
r'(selected best master clock|new foreign master)\s+([0-9a-fA-F\.:]+)',
|
if [ ${#offsets[@]} -gt $WINDOW ]; then
|
||||||
line
|
offsets=("${offsets[@]:1}")
|
||||||
)
|
delays=("${delays[@]:1}")
|
||||||
if m:
|
freqs=("${freqs[@]:1}")
|
||||||
master_events.append((timestamp, m.group(2)))
|
fi
|
||||||
|
|
||||||
return t, rms, freq, state_events, master_events
|
# --- stats ---
|
||||||
|
rms=$(printf "%s\n" "${offsets[@]}" | awk '{s+=$1*$1} END {printf "%.0f", sqrt(s/NR)}')
|
||||||
|
|
||||||
|
max=$(printf "%s\n" "${offsets[@]}" | \
|
||||||
|
awk 'BEGIN{m=0}{if($1>m)m=$1;if(-$1>m)m=-$1}END{print m}')
|
||||||
|
|
||||||
def plot_data(t, rms, freq, state_events, master_events, detail):
|
freq_avg=$(printf "%s\n" "${freqs[@]}" | awk '{s+=$1} END {printf "%.0f", s/NR}')
|
||||||
|
freq_std=$(printf "%s\n" "${freqs[@]}" | \
|
||||||
|
awk '{s+=$1; ss+=$1*$1} END {m=s/NR; printf "%.0f", sqrt(ss/NR - m*m)}')
|
||||||
|
|
||||||
if not t:
|
delay_avg=$(printf "%s\n" "${delays[@]}" | awk '{s+=$1} END {printf "%.0f", s/NR}')
|
||||||
messagebox.showerror("Error", "No valid RMS/freq data found.")
|
delay_std=$(printf "%s\n" "${delays[@]}" | \
|
||||||
return
|
awk '{s+=$1; ss+=$1*$1} END {m=s/NR; printf "%.0f", sqrt(ss/NR - m*m)}')
|
||||||
|
|
||||||
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True)
|
line="$now_fmt $HOST ptp4l@media1[$$]: [$uptime] rms $rms max $max freq $freq_avg +/- $freq_std delay $delay_avg +/- $delay_std"
|
||||||
|
|
||||||
ax1.plot(t, rms, color='black')
|
echo "$line" | tee -a "$LOGFILE"
|
||||||
ax1.set_ylabel("RMS (ns)")
|
|
||||||
ax1.grid()
|
|
||||||
|
|
||||||
ax2.plot(t, freq, color='black')
|
sleep 1
|
||||||
ax2.set_ylabel("Frequency deviation")
|
done
|
||||||
ax2.set_xlabel("Time")
|
|
||||||
ax2.grid()
|
|
||||||
|
|
||||||
# event lines
|
|
||||||
for ts, _ in state_events:
|
|
||||||
ax1.axvline(ts, color='blue', linestyle='--', alpha=0.7)
|
|
||||||
ax2.axvline(ts, color='blue', linestyle='--', alpha=0.7)
|
|
||||||
|
|
||||||
for ts, _ in master_events:
|
|
||||||
ax1.axvline(ts, color='red', linestyle='--', alpha=0.7)
|
|
||||||
ax2.axvline(ts, color='red', linestyle='--', alpha=0.7)
|
|
||||||
|
|
||||||
legend = [
|
|
||||||
Line2D([0], [0], color='black', label='RMS/Freq'),
|
|
||||||
Line2D([0], [0], color='blue', linestyle='--', label='State'),
|
|
||||||
Line2D([0], [0], color='red', linestyle='--', label='Master'),
|
|
||||||
]
|
|
||||||
ax1.legend(handles=legend)
|
|
||||||
|
|
||||||
multi_day = any(d.date() != t[0].date() for d in t)
|
|
||||||
|
|
||||||
if multi_day:
|
|
||||||
formatter = mdates.DateFormatter('%d %H:%M')
|
|
||||||
else:
|
|
||||||
formatter = mdates.DateFormatter('%H:%M:%S')
|
|
||||||
|
|
||||||
if detail == "Seconds":
|
|
||||||
locator = mdates.SecondLocator(interval=1)
|
|
||||||
elif detail == "5 sec":
|
|
||||||
locator = mdates.SecondLocator(interval=5)
|
|
||||||
elif detail == "10 sec":
|
|
||||||
locator = mdates.SecondLocator(interval=10)
|
|
||||||
elif detail == "30 sec":
|
|
||||||
locator = mdates.SecondLocator(interval=30)
|
|
||||||
elif detail == "1 min":
|
|
||||||
locator = mdates.MinuteLocator(interval=1)
|
|
||||||
else:
|
|
||||||
locator = mdates.AutoDateLocator(maxticks=20)
|
|
||||||
|
|
||||||
ax2.xaxis.set_major_locator(locator)
|
|
||||||
ax2.xaxis.set_major_formatter(formatter)
|
|
||||||
|
|
||||||
plt.xticks(rotation=45)
|
|
||||||
plt.tight_layout()
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
|
|
||||||
def open_file():
|
|
||||||
file_path = filedialog.askopenfilename(
|
|
||||||
title="Select log",
|
|
||||||
filetypes=[("Log files", "*.log *.txt"), ("All files", "*.*")]
|
|
||||||
)
|
|
||||||
|
|
||||||
if not file_path:
|
|
||||||
return
|
|
||||||
|
|
||||||
detail = detail_var.get()
|
|
||||||
|
|
||||||
t, rms, freq, state_events, master_events = parse_log(file_path)
|
|
||||||
|
|
||||||
plot_data(t, rms, freq, state_events, master_events, detail)
|
|
||||||
|
|
||||||
event_box.delete(1.0, tk.END)
|
|
||||||
|
|
||||||
events = []
|
|
||||||
|
|
||||||
for ts, label in state_events:
|
|
||||||
events.append((ts, f"{ts} | STATE {label}"))
|
|
||||||
|
|
||||||
for ts, label in master_events:
|
|
||||||
events.append((ts, f"{ts} | MASTER {label}"))
|
|
||||||
|
|
||||||
events.sort(key=lambda x: x[0])
|
|
||||||
|
|
||||||
if events:
|
|
||||||
for _, line in events:
|
|
||||||
event_box.insert(tk.END, line + "\n")
|
|
||||||
else:
|
|
||||||
event_box.insert(tk.END, "No events found.\n")
|
|
||||||
|
|
||||||
|
|
||||||
# GUI
|
|
||||||
root = tk.Tk()
|
|
||||||
root.title("PTP Log Viewer")
|
|
||||||
root.geometry("750x550")
|
|
||||||
|
|
||||||
frame = tk.Frame(root)
|
|
||||||
frame.pack(pady=5)
|
|
||||||
|
|
||||||
tk.Button(frame, text="Open Log", command=open_file).pack(side=tk.LEFT, padx=10)
|
|
||||||
|
|
||||||
detail_var = tk.StringVar(value="Auto")
|
|
||||||
|
|
||||||
tk.OptionMenu(
|
|
||||||
frame,
|
|
||||||
detail_var,
|
|
||||||
"Auto",
|
|
||||||
"Seconds",
|
|
||||||
"5 sec",
|
|
||||||
"10 sec",
|
|
||||||
"30 sec",
|
|
||||||
"1 min"
|
|
||||||
).pack(side=tk.LEFT)
|
|
||||||
|
|
||||||
event_box = scrolledtext.ScrolledText(root)
|
|
||||||
event_box.pack(fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
root.mainloop()
|
|
||||||
Reference in New Issue
Block a user