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