Compare commits

..

No commits in common. "master" and "v.0.0.0.1" have entirely different histories.

6 changed files with 93 additions and 213 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 0 B

View File

@ -1,5 +1,39 @@
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None block_cipher = None
a = Analysis(pathex=['netanalyzer', '.'], binaries=[], datas=[('NetAnalyzer.ico', '.')], hiddenimports=[], hookspath=[], runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False, scripts=['netanalyzer\\__main__.py'])
a = Analysis(
['netanalyzer/__main__.py'],
pathex=['.'],
binaries=[],
# Usa project_icon_filename nella sezione datas
datas=[('NetAnalyzer.ico', '.')],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [], name='NetAnalyzer', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, upx_exclude=[], runtime_tmpdir=None, console=True, icon='NetAnalyzer.ico', exclude_binaries=True)
coll = COLLECT(exe, a.binaries, a.zipfiles, a.datas, strip=False, upx=True, upx_exclude=[], name='NetAnalyzer') exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='NetAnalyzer',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
# Usa project_icon_filename per l'opzione icon
icon='NetAnalyzer.ico'
)

View File

@ -1,4 +1,4 @@
from gui.main_view import MainView from .gui.main_view import MainView
def main(): def main():
""" """

View File

@ -1,74 +0,0 @@
# -*- coding: utf-8 -*-
# File generated by PyInstaller GUI Wrapper. DO NOT EDIT MANUALLY.
# Contains build-time information scraped from Git (if available)
# and a helper function to format version strings.
import re
# --- Version Data (Generated) ---
__version__ = "v.0.0.0.2-0-gd4e6f34-dirty"
GIT_COMMIT_HASH = "d4e6f3487d230e5b2b1df98cf35f2e853bc2f38e"
GIT_BRANCH = "master"
BUILD_TIMESTAMP = "2025-11-19T13:05:49.512611+00:00"
IS_GIT_REPO = True
# --- Default Values (for comparison or fallback) ---
DEFAULT_VERSION = "0.0.0+unknown"
DEFAULT_COMMIT = "Unknown"
DEFAULT_BRANCH = "Unknown"
# --- Helper Function ---
def get_version_string(format_string=None):
"""
Returns a formatted string based on the build version information.
Args:
format_string (str, optional): A format string using placeholders.
Defaults to "{version} ({branch}/{commit_short})" if None.
Placeholders:
{{version}}: Full version string (e.g., 'v1.0.0-5-gabcdef-dirty')
{{tag}}: Clean tag part if exists (e.g., 'v1.0.0'), else DEFAULT_VERSION.
{{commit}}: Full Git commit hash.
{{commit_short}}: Short Git commit hash (7 chars).
{{branch}}: Git branch name.
{{dirty}}: '-dirty' if the repo was dirty, empty otherwise.
{{timestamp}}: Full build timestamp (ISO 8601 UTC).
{{timestamp_short}}: Build date only (YYYY-MM-DD).
{{is_git}}: 'Git' if IS_GIT_REPO is True, 'Unknown' otherwise.
Returns:
str: The formatted version string, or an error message if formatting fails.
"""
if format_string is None:
format_string = "{version} ({branch}/{commit_short})" # Default format
replacements = {}
try:
replacements['version'] = __version__ if __version__ else DEFAULT_VERSION
replacements['commit'] = GIT_COMMIT_HASH if GIT_COMMIT_HASH else DEFAULT_COMMIT
replacements['commit_short'] = GIT_COMMIT_HASH[:7] if GIT_COMMIT_HASH and len(GIT_COMMIT_HASH) >= 7 else DEFAULT_COMMIT
replacements['branch'] = GIT_BRANCH if GIT_BRANCH else DEFAULT_BRANCH
replacements['timestamp'] = BUILD_TIMESTAMP if BUILD_TIMESTAMP else "Unknown"
replacements['timestamp_short'] = BUILD_TIMESTAMP.split('T')[0] if BUILD_TIMESTAMP and 'T' in BUILD_TIMESTAMP else "Unknown"
replacements['is_git'] = "Git" if IS_GIT_REPO else "Unknown"
replacements['dirty'] = "-dirty" if __version__ and __version__.endswith('-dirty') else ""
tag = DEFAULT_VERSION
if __version__ and IS_GIT_REPO:
match = re.match(r'^(v?([0-9]+(?:\.[0-9]+)*))', __version__)
if match:
tag = match.group(1)
replacements['tag'] = tag
output_string = format_string
for placeholder, value in replacements.items():
pattern = re.compile(r'{{\s*' + re.escape(placeholder) + r'\s*}}')
output_string = pattern.sub(str(value), output_string)
if re.search(r'{\s*\w+\s*}', output_string):
pass # Or log a warning: print(f"Warning: Unreplaced placeholders found: {output_string}")
return output_string
except Exception as e:
return f"[Formatting Error: {e}]"

View File

@ -1,5 +1,3 @@
# NetPulseAnalyzer/core/analyzer.py
import scapy.all as scapy import scapy.all as scapy
import pandas as pd import pandas as pd
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
@ -33,6 +31,7 @@ class PacketAnalyzer:
if not self.packets: if not self.packets:
raise ValueError("No packets loaded. Call load_pcap() first.") raise ValueError("No packets loaded. Call load_pcap() first.")
# Extract timestamps for packets on port A and port B (destination ports)
times_a = [p.time for p in self.packets if p.haslayer(scapy.UDP) and p[scapy.UDP].dport == port_a] times_a = [p.time for p in self.packets if p.haslayer(scapy.UDP) and p[scapy.UDP].dport == port_a]
times_b = [p.time for p in self.packets if p.haslayer(scapy.UDP) and p[scapy.UDP].dport == port_b] times_b = [p.time for p in self.packets if p.haslayer(scapy.UDP) and p[scapy.UDP].dport == port_b]
@ -43,8 +42,13 @@ class PacketAnalyzer:
if min_len == 0: if min_len == 0:
return {"error": "No matching A/B packet pairs found."} return {"error": "No matching A/B packet pairs found."}
# Calculate the time deltas in milliseconds
processing_times_ms = (pd.Series(times_b[:min_len]) - pd.Series(times_a[:min_len])) * 1000 processing_times_ms = (pd.Series(times_b[:min_len]) - pd.Series(times_a[:min_len])) * 1000
# Use pandas to get a comprehensive set of statistics
stats = processing_times_ms.describe(percentiles=[.25, .5, .75, .95, .99]).to_dict() stats = processing_times_ms.describe(percentiles=[.25, .5, .75, .95, .99]).to_dict()
# Add raw data points for plotting
stats['data_points'] = processing_times_ms.tolist() stats['data_points'] = processing_times_ms.tolist()
return stats return stats
@ -56,15 +60,21 @@ class PacketAnalyzer:
if not self.packets: if not self.packets:
raise ValueError("No packets loaded.") raise ValueError("No packets loaded.")
# Filter packets for the specified port (either source or destination)
times = [p.time for p in self.packets if p.haslayer(scapy.UDP) and (p[scapy.UDP].sport == port or p[scapy.UDP].dport == port)] times = [p.time for p in self.packets if p.haslayer(scapy.UDP) and (p[scapy.UDP].sport == port or p[scapy.UDP].dport == port)]
if len(times) < 2: if len(times) < 2:
return {"error": f"Fewer than 2 packets found on port {port}. Cannot calculate interval."} return {"error": f"Fewer than 2 packets found on port {port}. Cannot calculate interval."}
# Calculate the difference between each timestamp and the previous one
gaps_ms = pd.Series(times).diff().dropna() * 1000 gaps_ms = pd.Series(times).diff().dropna() * 1000
# Calculate statistics
stats = gaps_ms.describe(percentiles=[.25, .5, .75, .95, .99]).to_dict() stats = gaps_ms.describe(percentiles=[.25, .5, .75, .95, .99]).to_dict()
# Jitter is often defined as the standard deviation of the packet delay variation
stats['jitter'] = stats.get('std', 0.0) stats['jitter'] = stats.get('std', 0.0)
stats['data_points'] = gaps_ms.tolist() stats['data_points'] = gaps_ms.tolist()
return stats return stats

View File

@ -1,11 +1,14 @@
# NetPulseAnalyzer/gui/main_view.py
import tkinter as tk import tkinter as tk
from tkinter import ttk, filedialog, messagebox from tkinter import ttk, filedialog, messagebox
import threading import threading
import os import os
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk # NUOVO IMPORT from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import pandas as pd import pandas as pd
from typing import List, Dict, Any from typing import Dict, Any
from ..core.analyzer import PacketAnalyzer from ..core.analyzer import PacketAnalyzer
@ -26,6 +29,7 @@ class MainView(tk.Tk):
main_frame = ttk.Frame(self, padding=10) main_frame = ttk.Frame(self, padding=10)
main_frame.pack(fill=tk.BOTH, expand=True) main_frame.pack(fill=tk.BOTH, expand=True)
# --- Top Controls Section ---
controls_frame = ttk.LabelFrame(main_frame, text="File Controls", padding=10) controls_frame = ttk.LabelFrame(main_frame, text="File Controls", padding=10)
controls_frame.pack(fill=tk.X, pady=(0, 10)) controls_frame.pack(fill=tk.X, pady=(0, 10))
@ -34,15 +38,18 @@ class MainView(tk.Tk):
self.load_button = ttk.Button(controls_frame, text="Open .pcapng File...", command=self._open_file) 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.load_button.pack(side=tk.RIGHT, padx=5)
# --- Notebook for different analysis types ---
self.results_notebook = ttk.Notebook(main_frame) self.results_notebook = ttk.Notebook(main_frame)
self.results_notebook.pack(fill=tk.BOTH, expand=True) self.results_notebook.pack(fill=tk.BOTH, expand=True)
# Create the two tabs
self.tab_processing_time = ttk.Frame(self.results_notebook) self.tab_processing_time = ttk.Frame(self.results_notebook)
self.tab_jitter = 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_processing_time, text=" Processing Time (A -> B) ")
self.results_notebook.add(self.tab_jitter, text=" Interval / Jitter Analysis ") self.results_notebook.add(self.tab_jitter, text=" Interval / Jitter Analysis ")
# Populate each tab with its specific widgets
self._create_processing_time_tab(self.tab_processing_time) self._create_processing_time_tab(self.tab_processing_time)
self._create_jitter_tab(self.tab_jitter) self._create_jitter_tab(self.tab_jitter)
@ -79,55 +86,29 @@ class MainView(tk.Tk):
self.proc_fig = plt.Figure(figsize=(5, 4), dpi=100) self.proc_fig = plt.Figure(figsize=(5, 4), dpi=100)
self.proc_ax = self.proc_fig.add_subplot(111) self.proc_ax = self.proc_fig.add_subplot(111)
self.proc_canvas = FigureCanvasTkAgg(self.proc_fig, master=plot_frame) 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) self.proc_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
def _create_jitter_tab(self, parent_tab): def _create_jitter_tab(self, parent_tab):
controls = ttk.LabelFrame(parent_tab, text="Analysis Parameters", padding=10) controls = ttk.LabelFrame(parent_tab, text="Analysis Parameters", padding=10)
controls.pack(fill=tk.X, padx=10, pady=10) controls.pack(fill=tk.X, padx=10, pady=10)
ports_frame = ttk.Frame(controls) ttk.Label(controls, text="Port to Analyze:").grid(row=0, column=0, padx=5, sticky='w')
ports_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) self.jitter_port_var = tk.StringVar(value="60012") # Default to server command port
ttk.Entry(controls, textvariable=self.jitter_port_var, width=10).grid(row=0, column=1)
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 = 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') self.jitter_analyze_button.grid(row=0, column=2, padx=20)
pane = ttk.PanedWindow(parent_tab, orient=tk.HORIZONTAL) pane = ttk.PanedWindow(parent_tab, orient=tk.HORIZONTAL)
pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
stats_frame = ttk.LabelFrame(pane, text="Interval Statistics (ms)", padding=10) stats_frame = ttk.LabelFrame(pane, text="Interval Statistics (ms)", padding=10)
pane.add(stats_frame, weight=2) pane.add(stats_frame, weight=1)
self.jitter_stats_tree = ttk.Treeview(stats_frame, columns=("Port", "Metric", "Value"), show="headings") self.jitter_stats_tree = ttk.Treeview(stats_frame, columns=("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("Metric", text="Metric")
self.jitter_stats_tree.heading("Value", text="Value (ms)") 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("Metric", width=120)
self.jitter_stats_tree.column("Value", width=100, anchor='e') self.jitter_stats_tree.column("Value", width=100, anchor='e')
self.jitter_stats_tree.pack(fill=tk.BOTH, expand=True) self.jitter_stats_tree.pack(fill=tk.BOTH, expand=True)
@ -138,31 +119,8 @@ class MainView(tk.Tk):
self.jitter_fig = plt.Figure(figsize=(5, 4), dpi=100) self.jitter_fig = plt.Figure(figsize=(5, 4), dpi=100)
self.jitter_ax = self.jitter_fig.add_subplot(111) self.jitter_ax = self.jitter_fig.add_subplot(111)
self.jitter_canvas = FigureCanvasTkAgg(self.jitter_fig, master=plot_frame) 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) 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): def _open_file(self):
filepath = filedialog.askopenfilename( filepath = filedialog.askopenfilename(
title="Select a capture file", title="Select a capture file",
@ -184,6 +142,7 @@ class MainView(tk.Tk):
).start() ).start()
def _create_progress_dialog(self, filepath: str) -> tk.Toplevel: def _create_progress_dialog(self, filepath: str) -> tk.Toplevel:
"""Creates a modal dialog with an indeterminate progress bar."""
dialog = tk.Toplevel(self) dialog = tk.Toplevel(self)
dialog.title("Loading...") dialog.title("Loading...")
dialog.geometry("400x150") dialog.geometry("400x150")
@ -200,6 +159,7 @@ class MainView(tk.Tk):
ttk.Label(dialog, text="Loading capture file:", padding=10).pack() 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=f"{filename} {size_str}", font=('Helvetica', 10, 'bold')).pack()
ttk.Label(dialog, text="This may take a moment...", padding=5).pack() ttk.Label(dialog, text="This may take a moment...", padding=5).pack()
progress_bar = ttk.Progressbar(dialog, mode='indeterminate', length=350) progress_bar = ttk.Progressbar(dialog, mode='indeterminate', length=350)
@ -214,6 +174,9 @@ class MainView(tk.Tk):
return dialog return dialog
def _load_file_thread(self, filepath: str, progress_dialog: tk.Toplevel): def _load_file_thread(self, filepath: str, progress_dialog: tk.Toplevel):
"""
Background thread function to load the file. Closes the dialog when done.
"""
try: try:
total_packets = self.analyzer.load_pcap(filepath) total_packets = self.analyzer.load_pcap(filepath)
self.after(0, self._on_load_complete, filepath, total_packets) self.after(0, self._on_load_complete, filepath, total_packets)
@ -225,6 +188,7 @@ class MainView(tk.Tk):
self.after(0, self.load_button.config, {"state": tk.NORMAL}) self.after(0, self.load_button.config, {"state": tk.NORMAL})
def _on_load_complete(self, filepath, total_packets): def _on_load_complete(self, filepath, total_packets):
"""GUI thread callback for when file loading is complete."""
self.filepath_label.config(text=f"File: {os.path.basename(filepath)} ({total_packets} packets)") self.filepath_label.config(text=f"File: {os.path.basename(filepath)} ({total_packets} packets)")
self.proc_analyze_button.config(state=tk.NORMAL) self.proc_analyze_button.config(state=tk.NORMAL)
self.jitter_analyze_button.config(state=tk.NORMAL) self.jitter_analyze_button.config(state=tk.NORMAL)
@ -237,45 +201,35 @@ class MainView(tk.Tk):
if "error" in stats: if "error" in stats:
messagebox.showerror("Analysis Error", stats["error"]) messagebox.showerror("Analysis Error", stats["error"])
return return
self._update_single_port_stats_table(self.proc_stats_tree, stats) self._update_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)") self._update_plot(self.proc_ax, self.proc_fig, self.proc_canvas, stats['data_points'], "Processing Time", "Time (ms)")
except Exception as e: except Exception as e:
messagebox.showerror("Error", f"Analysis failed: {e}") messagebox.showerror("Error", f"Analysis failed: {e}")
def _run_jitter_analysis(self): def _run_jitter_analysis(self):
selected_indices = self.jitter_ports_listbox.curselection() try:
if not selected_indices: port = int(self.jitter_port_var.get())
messagebox.showwarning("No Selection", "Please select one or more ports to analyze.") stats = self.analyzer.calculate_inter_packet_gap_stats(port)
return if "error" in stats:
messagebox.showerror("Analysis Error", stats["error"])
return
selected_ports = [self.jitter_ports_listbox.get(i) for i in selected_indices] # CORREZIONE: Usiamo la chiave 'std' per 'Jitter'
stats['Jitter'] = stats.get('std', 0.0)
all_stats: Dict[int, Dict[str, Any]] = {} self._update_stats_table(self.jitter_stats_tree, stats)
all_data_points: Dict[int, List[float]] = {} self._update_plot(self.jitter_ax, self.jitter_fig, self.jitter_canvas, stats['data_points'], f"Packet Interval (Port {port})", "Interval (ms)")
except Exception as e:
messagebox.showerror("Error", f"Analysis failed: {e}")
for port_str in selected_ports: def _update_stats_table(self, tree: ttk.Treeview, stats: Dict[str, Any]):
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(): for item in tree.get_children():
tree.delete(item) tree.delete(item)
# CORREZIONE: Usiamo 'std' ma lo etichettiamo come Jitter per chiarezza
metric_map = { metric_map = {
'count': 'Sample Count', 'mean': 'Mean', 'std': 'Std Dev', 'count': 'Sample Count', 'mean': 'Mean', 'std': 'Std Dev (Jitter)',
'min': 'Min', '25%': '25th Percentile', '50%': 'Median', 'min': 'Min', '25%': '25th Percentile', '50%': 'Median (50%)',
'75%': '75th Percentile', '95%': '95th Percentile', '99%': '99th Percentile', '75%': '75th Percentile', '95%': '95th Percentile', '99%': '99th Percentile',
'max': 'Max' 'max': 'Max'
} }
@ -288,35 +242,14 @@ class MainView(tk.Tk):
else: else:
tree.insert('', 'end', values=(name, f"{value:.4f}")) tree.insert('', 'end', values=(name, f"{value:.4f}"))
# --- NUOVA FUNZIONE per la tabella multi-porta --- def _update_plot(self, ax, fig, canvas, data_points, title, ylabel):
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.clear()
ax.plot(data_points, marker='.', linestyle='-', markersize=4) ax.plot(data_points, marker='.', linestyle='-', markersize=4)
if data_points: if data_points:
s = pd.Series(data_points) s = pd.Series(data_points)
mean_val, p99_val = s.mean(), s.quantile(0.99) mean_val = s.mean()
p99_val = s.quantile(0.99)
ax.axhline(y=mean_val, color='g', linestyle='--', label=f'Mean ({mean_val:.2f} ms)') 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.axhline(y=p99_val, color='r', linestyle='--', label=f'99th Percentile ({p99_val:.2f} ms)')
@ -327,26 +260,3 @@ class MainView(tk.Tk):
ax.legend() ax.legend()
fig.tight_layout() fig.tight_layout()
canvas.draw() 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()