diff --git a/PTPLogger/PTPLogger.sh b/PTPLogger/PTPLogger.sh index 000e8f9..d1e9636 100644 --- a/PTPLogger/PTPLogger.sh +++ b/PTPLogger/PTPLogger.sh @@ -1,73 +1,175 @@ -#!/bin/bash +import re +import datetime as dt +import matplotlib.pyplot as plt +import matplotlib.dates as mdates +from matplotlib.lines import Line2D -#### Tested running on a 1232 +import tkinter as tk +from tkinter import filedialog, messagebox, scrolledtext -SOCK="/var/run/ptp4l_media1" -HOST=$(hostname) -LOGFILE="ptp_media1.log" -WINDOW=60 -offsets=() -delays=() -freqs=() +def parse_log(file_path): + t, rms, freq = [], [], [] + state_events = [] + master_events = [] -prev_offset="" -prev_time="" + last_timestamp = None -while true; do - now_fmt=$(date +"%b %d %T") - uptime=$(awk '{print $1}' /proc/uptime) - now_ns=$(date +%s%N) + with open(file_path, encoding='utf-8', errors='ignore') as f: + for line in f: - # --- get offset --- - ts_np=$(pmc -u -s $SOCK "GET TIME_STATUS_NP") - offset=$(echo "$ts_np" | awk '/master_offset/ {print $2}') + ts_match = re.match(r'^(\w+ \d+ \d+:\d+:\d+)', line) + if ts_match: + try: + timestr = ts_match.group(1) + " 2026" + last_timestamp = dt.datetime.strptime(timestr, "%b %d %H:%M:%S %Y") + except: + pass - # --- get delay --- - cds=$(pmc -u -s $SOCK "GET CURRENT_DATA_SET") - delay=$(echo "$cds" | awk '/meanPathDelay/ {print $2}') + timestamp = last_timestamp - # --- compute freq (ns/sec ≈ ppb-like behaviour) --- - 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 + if not timestamp: + continue - prev_offset=$offset - prev_time=$now_ns + m = re.search(r'rms\s+(\d+).*freq\s+([+-]?\d+)', line) + if m: + t.append(timestamp) + rms.append(int(m.group(1))) + freq.append(int(m.group(2))) - # store - offsets+=("$offset") - delays+=("$delay") - freqs+=("$freq") + m = re.search(r'port \d+: (\w+) to (\w+)', line) + if m: + state_events.append((timestamp, f"{m.group(1)}→{m.group(2)}")) - # trim window - if [ ${#offsets[@]} -gt $WINDOW ]; then - offsets=("${offsets[@]:1}") - delays=("${delays[@]:1}") - freqs=("${freqs[@]:1}") - fi + m = re.search( + r'(selected best master clock|new foreign master)\s+([0-9a-fA-F\.:]+)', + line + ) + if m: + master_events.append((timestamp, m.group(2))) - # --- stats --- - rms=$(printf "%s\n" "${offsets[@]}" | awk '{s+=$1*$1} END {printf "%.0f", sqrt(s/NR)}') + return t, rms, freq, state_events, master_events - max=$(printf "%s\n" "${offsets[@]}" | \ - awk 'BEGIN{m=0}{if($1>m)m=$1;if(-$1>m)m=-$1}END{print m}') - 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)}') +def plot_data(t, rms, freq, state_events, master_events, detail): - delay_avg=$(printf "%s\n" "${delays[@]}" | awk '{s+=$1} END {printf "%.0f", s/NR}') - delay_std=$(printf "%s\n" "${delays[@]}" | \ - awk '{s+=$1; ss+=$1*$1} END {m=s/NR; printf "%.0f", sqrt(ss/NR - m*m)}') + if not t: + messagebox.showerror("Error", "No valid RMS/freq data found.") + return - line="$now_fmt $HOST ptp4l@media1[$$]: [$uptime] rms $rms max $max freq $freq_avg +/- $freq_std delay $delay_avg +/- $delay_std" + fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True) - echo "$line" | tee -a "$LOGFILE" + ax1.plot(t, rms, color='black') + ax1.set_ylabel("RMS (ns)") + ax1.grid() - sleep 1 -done \ No newline at end of file + ax2.plot(t, freq, color='black') + ax2.set_ylabel("Frequency deviation") + 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() \ No newline at end of file