352 lines
15 KiB
Python
352 lines
15 KiB
Python
import tkinter as tk
|
|
from tkinter import ttk, filedialog, messagebox
|
|
import threading
|
|
import os
|
|
import matplotlib.pyplot as plt
|
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk # NUOVO IMPORT
|
|
import pandas as pd
|
|
from typing import List, Dict, Any
|
|
|
|
from ..core.analyzer import PacketAnalyzer
|
|
|
|
class MainView(tk.Tk):
|
|
"""
|
|
Main window for the NetPulseAnalyzer application.
|
|
"""
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.title("NetPulseAnalyzer")
|
|
self.geometry("1200x800")
|
|
|
|
self.analyzer = PacketAnalyzer()
|
|
|
|
self._create_widgets()
|
|
|
|
def _create_widgets(self):
|
|
main_frame = ttk.Frame(self, padding=10)
|
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
controls_frame = ttk.LabelFrame(main_frame, text="File Controls", padding=10)
|
|
controls_frame.pack(fill=tk.X, pady=(0, 10))
|
|
|
|
self.filepath_label = ttk.Label(controls_frame, text="No file loaded.")
|
|
self.filepath_label.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
|
|
self.load_button = ttk.Button(controls_frame, text="Open .pcapng File...", command=self._open_file)
|
|
self.load_button.pack(side=tk.RIGHT, padx=5)
|
|
|
|
self.results_notebook = ttk.Notebook(main_frame)
|
|
self.results_notebook.pack(fill=tk.BOTH, expand=True)
|
|
|
|
self.tab_processing_time = ttk.Frame(self.results_notebook)
|
|
self.tab_jitter = ttk.Frame(self.results_notebook)
|
|
|
|
self.results_notebook.add(self.tab_processing_time, text=" Processing Time (A -> B) ")
|
|
self.results_notebook.add(self.tab_jitter, text=" Interval / Jitter Analysis ")
|
|
|
|
self._create_processing_time_tab(self.tab_processing_time)
|
|
self._create_jitter_tab(self.tab_jitter)
|
|
|
|
def _create_processing_time_tab(self, parent_tab):
|
|
controls = ttk.LabelFrame(parent_tab, text="Analysis Parameters", padding=10)
|
|
controls.pack(fill=tk.X, padx=10, pady=10)
|
|
|
|
ttk.Label(controls, text="Port A (Start):").grid(row=0, column=0, padx=5, sticky='w')
|
|
self.proc_port_a_var = tk.StringVar(value="55001")
|
|
ttk.Entry(controls, textvariable=self.proc_port_a_var, width=10).grid(row=0, column=1)
|
|
|
|
ttk.Label(controls, text="Port B (End):").grid(row=0, column=2, padx=(20, 5), sticky='w')
|
|
self.proc_port_b_var = tk.StringVar(value="55002")
|
|
ttk.Entry(controls, textvariable=self.proc_port_b_var, width=10).grid(row=0, column=3)
|
|
|
|
self.proc_analyze_button = ttk.Button(controls, text="Analyze", command=self._run_processing_time_analysis, state=tk.DISABLED)
|
|
self.proc_analyze_button.grid(row=0, column=4, padx=20)
|
|
|
|
pane = ttk.PanedWindow(parent_tab, orient=tk.HORIZONTAL)
|
|
pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
|
|
|
|
stats_frame = ttk.LabelFrame(pane, text="Statistics (ms)", padding=10)
|
|
pane.add(stats_frame, weight=1)
|
|
|
|
self.proc_stats_tree = ttk.Treeview(stats_frame, columns=("Metric", "Value"), show="headings")
|
|
self.proc_stats_tree.heading("Metric", text="Metric")
|
|
self.proc_stats_tree.heading("Value", text="Value (ms)")
|
|
self.proc_stats_tree.column("Metric", width=120)
|
|
self.proc_stats_tree.column("Value", width=100, anchor='e')
|
|
self.proc_stats_tree.pack(fill=tk.BOTH, expand=True)
|
|
|
|
plot_frame = ttk.LabelFrame(pane, text="Plot", padding=10)
|
|
pane.add(plot_frame, weight=3)
|
|
|
|
self.proc_fig = plt.Figure(figsize=(5, 4), dpi=100)
|
|
self.proc_ax = self.proc_fig.add_subplot(111)
|
|
|
|
self.proc_canvas = FigureCanvasTkAgg(self.proc_fig, master=plot_frame)
|
|
|
|
# --- NUOVA PARTE: Toolbar per il grafico ---
|
|
toolbar = NavigationToolbar2Tk(self.proc_canvas, plot_frame)
|
|
toolbar.update()
|
|
# --- FINE NUOVA PARTE ---
|
|
|
|
self.proc_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
|
|
|
|
def _create_jitter_tab(self, parent_tab):
|
|
controls = ttk.LabelFrame(parent_tab, text="Analysis Parameters", padding=10)
|
|
controls.pack(fill=tk.X, padx=10, pady=10)
|
|
|
|
ports_frame = ttk.Frame(controls)
|
|
ports_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
|
|
ttk.Label(ports_frame, text="Ports to Analyze (use Ctrl/Shift to multi-select):").pack(anchor='w')
|
|
|
|
self.jitter_ports_listbox = tk.Listbox(ports_frame, selectmode=tk.EXTENDED, height=4)
|
|
for port in ["60012", "55001", "55002"]:
|
|
self.jitter_ports_listbox.insert(tk.END, port)
|
|
self.jitter_ports_listbox.pack(side=tk.LEFT, fill=tk.X, expand=True, pady=5)
|
|
|
|
self.jitter_ports_listbox.selection_set(0)
|
|
self.jitter_ports_listbox.selection_set(2)
|
|
self.jitter_ports_listbox.selection_set(3)
|
|
|
|
add_port_frame = ttk.Frame(ports_frame)
|
|
add_port_frame.pack(side=tk.LEFT, padx=10, fill='y')
|
|
self.jitter_custom_port_var = tk.StringVar()
|
|
custom_port_entry = ttk.Entry(add_port_frame, textvariable=self.jitter_custom_port_var, width=10)
|
|
custom_port_entry.pack()
|
|
ttk.Button(add_port_frame, text="Add Port", command=self._add_jitter_port).pack(pady=5)
|
|
|
|
self.jitter_analyze_button = ttk.Button(controls, text="Analyze Jitter", command=self._run_jitter_analysis, state=tk.DISABLED)
|
|
self.jitter_analyze_button.pack(side=tk.LEFT, padx=20, anchor='center')
|
|
|
|
pane = ttk.PanedWindow(parent_tab, orient=tk.HORIZONTAL)
|
|
pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
|
|
|
|
stats_frame = ttk.LabelFrame(pane, text="Interval Statistics (ms)", padding=10)
|
|
pane.add(stats_frame, weight=2)
|
|
|
|
self.jitter_stats_tree = ttk.Treeview(stats_frame, columns=("Port", "Metric", "Value"), show="headings")
|
|
self.jitter_stats_tree.heading("Port", text="Port")
|
|
self.jitter_stats_tree.heading("Metric", text="Metric")
|
|
self.jitter_stats_tree.heading("Value", text="Value (ms)")
|
|
self.jitter_stats_tree.column("Port", width=80, anchor='center')
|
|
self.jitter_stats_tree.column("Metric", width=120)
|
|
self.jitter_stats_tree.column("Value", width=100, anchor='e')
|
|
self.jitter_stats_tree.pack(fill=tk.BOTH, expand=True)
|
|
|
|
plot_frame = ttk.LabelFrame(pane, text="Packet Interval (Jitter) Plot", padding=10)
|
|
pane.add(plot_frame, weight=3)
|
|
|
|
self.jitter_fig = plt.Figure(figsize=(5, 4), dpi=100)
|
|
self.jitter_ax = self.jitter_fig.add_subplot(111)
|
|
self.jitter_canvas = FigureCanvasTkAgg(self.jitter_fig, master=plot_frame)
|
|
|
|
# --- NUOVA PARTE: Toolbar per il grafico jitter ---
|
|
toolbar = NavigationToolbar2Tk(self.jitter_canvas, plot_frame)
|
|
toolbar.update()
|
|
# --- FINE NUOVA PARTE ---
|
|
|
|
self.jitter_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
|
|
|
|
def _add_jitter_port(self):
|
|
port_str = self.jitter_custom_port_var.get().strip()
|
|
if not port_str:
|
|
return
|
|
try:
|
|
port_num = int(port_str)
|
|
if not (0 < port_num < 65536):
|
|
raise ValueError("Port out of range")
|
|
|
|
if port_str not in self.jitter_ports_listbox.get(0, tk.END):
|
|
self.jitter_ports_listbox.insert(tk.END, port_str)
|
|
|
|
self.jitter_custom_port_var.set("")
|
|
except ValueError:
|
|
messagebox.showerror("Invalid Port", "Please enter a valid port number (1-65535).")
|
|
|
|
# --- Le funzioni di caricamento file rimangono invariate ---
|
|
def _open_file(self):
|
|
filepath = filedialog.askopenfilename(
|
|
title="Select a capture file",
|
|
filetypes=[("Wireshark/Scapy Captures", "*.pcapng *.pcap"), ("All files", "*.*")]
|
|
)
|
|
if not filepath:
|
|
return
|
|
|
|
self.load_button.config(state=tk.DISABLED)
|
|
self.proc_analyze_button.config(state=tk.DISABLED)
|
|
self.jitter_analyze_button.config(state=tk.DISABLED)
|
|
|
|
progress_dialog = self._create_progress_dialog(filepath)
|
|
|
|
threading.Thread(
|
|
target=self._load_file_thread,
|
|
args=(filepath, progress_dialog),
|
|
daemon=True
|
|
).start()
|
|
|
|
def _create_progress_dialog(self, filepath: str) -> tk.Toplevel:
|
|
dialog = tk.Toplevel(self)
|
|
dialog.title("Loading...")
|
|
dialog.geometry("400x150")
|
|
dialog.transient(self)
|
|
dialog.grab_set()
|
|
dialog.resizable(False, False)
|
|
|
|
filename = os.path.basename(filepath)
|
|
try:
|
|
file_size_mb = os.path.getsize(filepath) / (1024 * 1024)
|
|
size_str = f"({file_size_mb:.2f} MB)"
|
|
except OSError:
|
|
size_str = ""
|
|
|
|
ttk.Label(dialog, text="Loading capture file:", padding=10).pack()
|
|
ttk.Label(dialog, text=f"{filename} {size_str}", font=('Helvetica', 10, 'bold')).pack()
|
|
ttk.Label(dialog, text="This may take a moment...", padding=5).pack()
|
|
|
|
progress_bar = ttk.Progressbar(dialog, mode='indeterminate', length=350)
|
|
progress_bar.pack(pady=20)
|
|
progress_bar.start(10)
|
|
|
|
self.update_idletasks()
|
|
x = self.winfo_x() + (self.winfo_width() // 2) - (dialog.winfo_width() // 2)
|
|
y = self.winfo_y() + (self.winfo_height() // 2) - (dialog.winfo_height() // 2)
|
|
dialog.geometry(f"+{x}+{y}")
|
|
|
|
return dialog
|
|
|
|
def _load_file_thread(self, filepath: str, progress_dialog: tk.Toplevel):
|
|
try:
|
|
total_packets = self.analyzer.load_pcap(filepath)
|
|
self.after(0, self._on_load_complete, filepath, total_packets)
|
|
except Exception as e:
|
|
self.after(0, messagebox.showerror, "Load Error", f"Failed to read file:\n{e}")
|
|
self.after(0, self.filepath_label.config, {"text": "File load failed."})
|
|
finally:
|
|
self.after(0, progress_dialog.destroy)
|
|
self.after(0, self.load_button.config, {"state": tk.NORMAL})
|
|
|
|
def _on_load_complete(self, filepath, total_packets):
|
|
self.filepath_label.config(text=f"File: {os.path.basename(filepath)} ({total_packets} packets)")
|
|
self.proc_analyze_button.config(state=tk.NORMAL)
|
|
self.jitter_analyze_button.config(state=tk.NORMAL)
|
|
|
|
def _run_processing_time_analysis(self):
|
|
try:
|
|
port_a = int(self.proc_port_a_var.get())
|
|
port_b = int(self.proc_port_b_var.get())
|
|
stats = self.analyzer.calculate_ab_processing_stats(port_a, port_b)
|
|
if "error" in stats:
|
|
messagebox.showerror("Analysis Error", stats["error"])
|
|
return
|
|
self._update_single_port_stats_table(self.proc_stats_tree, stats)
|
|
self._update_single_series_plot(self.proc_ax, self.proc_fig, self.proc_canvas, stats['data_points'], "Processing Time", "Time (ms)")
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"Analysis failed: {e}")
|
|
|
|
def _run_jitter_analysis(self):
|
|
selected_indices = self.jitter_ports_listbox.curselection()
|
|
if not selected_indices:
|
|
messagebox.showwarning("No Selection", "Please select one or more ports to analyze.")
|
|
return
|
|
|
|
selected_ports = [self.jitter_ports_listbox.get(i) for i in selected_indices]
|
|
|
|
all_stats: Dict[int, Dict[str, Any]] = {}
|
|
all_data_points: Dict[int, List[float]] = {}
|
|
|
|
for port_str in selected_ports:
|
|
try:
|
|
port = int(port_str)
|
|
stats = self.analyzer.calculate_inter_packet_gap_stats(port)
|
|
if "error" not in stats:
|
|
all_stats[port] = stats
|
|
all_data_points[port] = stats['data_points']
|
|
else:
|
|
messagebox.showwarning("Analysis Warning", f"Could not analyze port {port}:\n{stats['error']}")
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"Analysis for port {port} failed: {e}")
|
|
|
|
if all_stats:
|
|
self._update_multi_port_stats_table(all_stats)
|
|
self._update_multi_series_plot(all_data_points)
|
|
|
|
def _update_single_port_stats_table(self, tree: ttk.Treeview, stats: Dict[str, Any]):
|
|
for item in tree.get_children():
|
|
tree.delete(item)
|
|
|
|
metric_map = {
|
|
'count': 'Sample Count', 'mean': 'Mean', 'std': 'Std Dev',
|
|
'min': 'Min', '25%': '25th Percentile', '50%': 'Median',
|
|
'75%': '75th Percentile', '95%': '95th Percentile', '99%': '99th Percentile',
|
|
'max': 'Max'
|
|
}
|
|
|
|
for key, name in metric_map.items():
|
|
if key in stats:
|
|
value = stats[key]
|
|
if key == 'count':
|
|
tree.insert('', 'end', values=(name, f"{int(value)}"))
|
|
else:
|
|
tree.insert('', 'end', values=(name, f"{value:.4f}"))
|
|
|
|
# --- NUOVA FUNZIONE per la tabella multi-porta ---
|
|
def _update_multi_port_stats_table(self, all_stats: Dict[int, Dict[str, Any]]):
|
|
tree = self.jitter_stats_tree
|
|
for item in tree.get_children():
|
|
tree.delete(item)
|
|
|
|
metric_map = {'count': 'Samples', 'mean': 'Mean', 'std': 'Std Dev (Jitter)', 'min': 'Min', 'max': 'Max', '50%': 'Median'}
|
|
|
|
for port, stats in sorted(all_stats.items()):
|
|
# Add a header row for the port
|
|
tree.insert('', 'end', values=(port, '', ''), tags=('header',))
|
|
for key, name in metric_map.items():
|
|
if key in stats:
|
|
value = stats[key]
|
|
if key == 'count':
|
|
tree.insert('', 'end', values=('', name, f"{int(value)}"))
|
|
else:
|
|
tree.insert('', 'end', values=('', name, f"{value:.4f}"))
|
|
|
|
tree.tag_configure('header', font=('Helvetica', 10, 'bold'), background='#eee')
|
|
|
|
|
|
def _update_single_series_plot(self, ax, fig, canvas, data_points, title, ylabel):
|
|
ax.clear()
|
|
ax.plot(data_points, marker='.', linestyle='-', markersize=4)
|
|
|
|
if data_points:
|
|
s = pd.Series(data_points)
|
|
mean_val, p99_val = s.mean(), s.quantile(0.99)
|
|
ax.axhline(y=mean_val, color='g', linestyle='--', label=f'Mean ({mean_val:.2f} ms)')
|
|
ax.axhline(y=p99_val, color='r', linestyle='--', label=f'99th Percentile ({p99_val:.2f} ms)')
|
|
|
|
ax.set_title(title)
|
|
ax.set_xlabel("Packet Sequence")
|
|
ax.set_ylabel(ylabel)
|
|
ax.grid(True)
|
|
ax.legend()
|
|
fig.tight_layout()
|
|
canvas.draw()
|
|
|
|
# --- NUOVA FUNZIONE per il grafico multi-serie ---
|
|
def _update_multi_series_plot(self, all_data_points: Dict[int, List[float]]):
|
|
ax, fig, canvas = self.jitter_ax, self.jitter_fig, self.jitter_canvas
|
|
ax.clear()
|
|
|
|
colors = plt.rcParams['axes.prop_cycle'].by_key()['color']
|
|
|
|
for i, (port, data_points) in enumerate(sorted(all_data_points.items())):
|
|
if data_points:
|
|
color = colors[i % len(colors)]
|
|
ax.plot(data_points, marker='.', linestyle='-', markersize=3, label=f'Port {port}', color=color, alpha=0.8)
|
|
|
|
ax.set_title("Packet Interval Comparison")
|
|
ax.set_xlabel("Packet Sequence")
|
|
ax.set_ylabel("Interval (ms)")
|
|
ax.grid(True, which='both', linestyle='--', linewidth=0.5)
|
|
|
|
if all_data_points:
|
|
ax.legend()
|
|
|
|
fig.tight_layout()
|
|
canvas.draw() |