diff --git a/PTPLogger/PTPLogger_Graphing.py b/PTPLogger/PTPLogger_Graphing.py index 5d72f2c..806f3bc 100644 --- a/PTPLogger/PTPLogger_Graphing.py +++ b/PTPLogger/PTPLogger_Graphing.py @@ -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() \ No newline at end of file