SXXXXXXX_NetAnalyzer/netanalyzer/gui/main_view.py
2025-11-18 16:04:23 +01:00

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()