Update PTPLogger/PTPLogger_Graphing.py

This commit is contained in:
2026-06-09 11:37:31 +10:00
parent 91d1d498cc
commit 3c5b9cb96b
+149 -41
View File
@@ -1,85 +1,193 @@
import re import re
import datetime as dt import datetime as dt
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import tkinter as tk import matplotlib.dates as mdates
from tkinter import filedialog, messagebox from matplotlib.lines import Line2D
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
# ===========================
# PARSE LOG
# ===========================
def parse_log(file_path): def parse_log(file_path):
t = [] t, rms, freq = [], [], []
rms = [] state_events = []
freq = [] master_events = []
last_timestamp = None
with open(file_path, encoding='utf-8', errors='ignore') as f: with open(file_path, encoding='utf-8', errors='ignore') as f:
for line in f: for line in f:
# Match only valid ptp4l stat lines
match = re.search( # --- Try syslog timestamp ---
r'^(\w+ \d+ \d+:\d+:\d+).*rms (\d+).*freq ([+-]?\d+)', ts_match = re.match(r'^(\w+ \d+ \d+:\d+:\d+)', line)
line if ts_match:
)
if match:
try: try:
# Add year (needed for parsing) timestr = ts_match.group(1) + " 2026"
timestr = match.group(1) + " 2026" last_timestamp = dt.datetime.strptime(timestr, "%b %d %H:%M:%S %Y")
timestamp = dt.datetime.strptime( except:
timestr, "%b %d %H:%M:%S %Y"
)
t.append(timestamp)
rms.append(int(match.group(2)))
freq.append(int(match.group(3)))
except Exception:
# Skip malformed lines safely
pass pass
return t, rms, freq timestamp = last_timestamp # fallback for ptp4l lines
# --- RMS/FREQ ---
m = re.search(r'rms\s+(\d+).*freq\s+([+-]?\d+)', line)
if m and timestamp:
t.append(timestamp)
rms.append(int(m.group(1)))
freq.append(int(m.group(2)))
# --- STATE CHANGE ---
m = re.search(r'port \d+: (\w+) to (\w+)', line)
if m and timestamp:
state_events.append((timestamp, f"{m.group(1)}{m.group(2)}"))
# --- MASTER CHANGE ---
m = re.search(
r'(selected best master clock|new foreign master)\s+([0-9a-fA-F\.:]+)',
line
)
if m and timestamp:
master_events.append((timestamp, m.group(2)))
return t, rms, freq, state_events, master_events
def plot_data(t, rms, freq): # ===========================
# PLOT
# ===========================
def plot_data(t, rms, freq, state_events, master_events, detail):
if not t: if not t:
messagebox.showerror("Error", "No valid PTP lines found.") messagebox.showerror("Error", "No valid RMS/freq data found.")
return return
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True) fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True)
ax1.plot(t, rms) ax1.plot(t, rms, color='black')
ax1.set_ylabel("RMS (ns)") ax1.set_ylabel("RMS (ns)")
ax1.set_title("PTP RMS Offset") ax1.grid()
ax2.plot(t, freq) ax2.plot(t, freq, color='black')
ax2.set_ylabel("Frequency deviation") ax2.set_ylabel("Frequency deviation")
ax2.set_xlabel("Time") ax2.set_xlabel("Time")
ax2.set_title("PTP Frequency Deviation") ax2.grid()
plt.xticks(rotation=30) # --- EVENTS ---
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 ---
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)
# --- SAFE DATE FORMAT ---
if len(t) > 0:
multi_day = any(d.date() != t[0].date() for d in t)
else:
multi_day = False
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.tight_layout()
plt.show() plt.show(block=False)
# ===========================
# LOAD FILE
# ===========================
def open_file(): def open_file():
file_path = filedialog.askopenfilename( file_path = filedialog.askopenfilename(
title="Select PTP log file", title="Select log",
filetypes=[("Log files", "*.log *.txt"), ("All files", "*.*")] filetypes=[("Log files", "*.log *.txt"), ("All files", "*.*")]
) )
if not file_path: if not file_path:
return return
t, rms, freq = parse_log(file_path) detail = detail_var.get()
plot_data(t, rms, freq) t, rms, freq, state_events, master_events = parse_log(file_path)
plot_data(t, rms, freq, state_events, master_events, detail)
# --- EVENT LIST ---
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 setup --- # ===========================
# GUI
# ===========================
root = tk.Tk() root = tk.Tk()
root.title("PTP Log Viewer") root.title("PTP Log Viewer")
root.geometry("750x550")
root.geometry("300x150") frame = tk.Frame(root)
frame.pack(pady=5)
label = tk.Label(root, text="PTP Log Plotter", font=("Arial", 14)) tk.Button(frame, text="Open Log", command=open_file).pack(side=tk.LEFT, padx=10)
label.pack(pady=10)
btn = tk.Button(root, text="Open Log File", command=open_file) detail_var = tk.StringVar(value="Auto")
btn.pack(pady=10)
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() root.mainloop()