Update PTPLogger/PTPLogger_Graphing.py
This commit is contained in:
+149
-41
@@ -1,85 +1,193 @@
|
||||
import re
|
||||
import datetime as dt
|
||||
import matplotlib.pyplot as plt
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, messagebox
|
||||
import matplotlib.dates as mdates
|
||||
from matplotlib.lines import Line2D
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, messagebox, scrolledtext
|
||||
|
||||
|
||||
# ===========================
|
||||
# PARSE LOG
|
||||
# ===========================
|
||||
def parse_log(file_path):
|
||||
t = []
|
||||
rms = []
|
||||
freq = []
|
||||
t, rms, freq = [], [], []
|
||||
state_events = []
|
||||
master_events = []
|
||||
|
||||
last_timestamp = None
|
||||
|
||||
with open(file_path, encoding='utf-8', errors='ignore') as f:
|
||||
for line in f:
|
||||
# Match only valid ptp4l stat lines
|
||||
match = re.search(
|
||||
r'^(\w+ \d+ \d+:\d+:\d+).*rms (\d+).*freq ([+-]?\d+)',
|
||||
line
|
||||
)
|
||||
if match:
|
||||
|
||||
# --- Try syslog timestamp ---
|
||||
ts_match = re.match(r'^(\w+ \d+ \d+:\d+:\d+)', line)
|
||||
if ts_match:
|
||||
try:
|
||||
# Add year (needed for parsing)
|
||||
timestr = match.group(1) + " 2026"
|
||||
timestamp = dt.datetime.strptime(
|
||||
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
|
||||
timestr = ts_match.group(1) + " 2026"
|
||||
last_timestamp = dt.datetime.strptime(timestr, "%b %d %H:%M:%S %Y")
|
||||
except:
|
||||
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:
|
||||
messagebox.showerror("Error", "No valid PTP lines found.")
|
||||
messagebox.showerror("Error", "No valid RMS/freq data found.")
|
||||
return
|
||||
|
||||
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_title("PTP RMS Offset")
|
||||
ax1.grid()
|
||||
|
||||
ax2.plot(t, freq)
|
||||
ax2.plot(t, freq, color='black')
|
||||
ax2.set_ylabel("Frequency deviation")
|
||||
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.show()
|
||||
plt.show(block=False)
|
||||
|
||||
|
||||
# ===========================
|
||||
# LOAD FILE
|
||||
# ===========================
|
||||
def open_file():
|
||||
file_path = filedialog.askopenfilename(
|
||||
title="Select PTP log file",
|
||||
title="Select log",
|
||||
filetypes=[("Log files", "*.log *.txt"), ("All files", "*.*")]
|
||||
)
|
||||
|
||||
if not file_path:
|
||||
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.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))
|
||||
label.pack(pady=10)
|
||||
tk.Button(frame, text="Open Log", command=open_file).pack(side=tk.LEFT, padx=10)
|
||||
|
||||
btn = tk.Button(root, text="Open Log File", command=open_file)
|
||||
btn.pack(pady=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