dark compact theme

This commit is contained in:
2025-07-26 00:02:25 -04:00
parent d77dd386f3
commit e9573d0e5f
6 changed files with 1244 additions and 387 deletions

697
analyzer/gui/dock_panels.py Normal file
View File

@@ -0,0 +1,697 @@
"""
Dockable panels for StreamLens GUI
"""
import os
from typing import Optional, List, TYPE_CHECKING, Dict
from PySide6.QtWidgets import (
QDockWidget, QWidget, QVBoxLayout, QHBoxLayout, QTableWidget,
QTableWidgetItem, QLabel, QHeaderView, QSplitter, QTextEdit
)
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QFont, QColor
try:
# Matplotlib integration - lazy loaded
matplotlib = None
FigureCanvas = None
NavigationToolbar = None
Figure = None
plt = None
def _ensure_matplotlib_gui_loaded():
"""Lazy load matplotlib for GUI mode"""
global matplotlib, FigureCanvas, NavigationToolbar, Figure, plt
if matplotlib is not None:
return True
try:
import matplotlib as mpl
matplotlib = mpl
matplotlib.use('Qt5Agg') # Use Qt backend for matplotlib
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FC
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NT
from matplotlib.figure import Figure as Fig
import matplotlib.pyplot as pyplot
# Turn off interactive mode to prevent floating windows
pyplot.ioff()
# Ensure no figure windows are created
mpl.rcParams['figure.max_open_warning'] = 0
mpl.rcParams['backend'] = 'Qt5Agg'
# Prevent any automatic figure display
mpl.pyplot.show = lambda *args, **kwargs: None
pyplot.show = lambda *args, **kwargs: None
# Also prevent figure.show()
original_figure_show = Fig.show
Fig.show = lambda self, *args, **kwargs: None
FigureCanvas = FC
NavigationToolbar = NT
Figure = Fig
plt = pyplot
return True
except ImportError as e:
print(f"Matplotlib GUI integration not available: {e}")
return False
except ImportError as e:
print(f"GUI dependencies not available: {e}")
if TYPE_CHECKING:
from ..analysis.core import EthernetAnalyzer
from ..models.flow_stats import FlowStats
class FlowListDockWidget(QWidget):
"""Widget containing the flow list and file information with integrated plots"""
def __init__(self, parent=None):
super().__init__(parent)
self.analyzer: Optional['EthernetAnalyzer'] = None
self.integrated_mode = True # Always use integrated mode
self._signal_cache = {} # Cache for processed signal data
# Set up compact main layout
layout = QVBoxLayout()
layout.setContentsMargins(4, 4, 4, 4) # Tight margins
layout.setSpacing(2) # Minimal spacing between elements
# Compact file info section
self.file_info_label = QLabel("No file loaded")
self.file_info_label.setWordWrap(True)
self.file_info_label.setStyleSheet("""
QLabel {
padding: 6px 12px;
background-color: #2d2d2d;
border: 1px solid #404040;
border-radius: 6px;
font-size: 11px;
color: #cccccc;
font-weight: 500;
}
""")
self.file_info_label.setMaximumHeight(35) # Compact height
layout.addWidget(self.file_info_label)
# Compact flow table with comprehensive data
self.flows_table = QTableWidget()
self.flows_table.setColumnCount(10) # More columns for dense data
self.flows_table.setHorizontalHeaderLabels([
"Source IP", "Dest IP", "Pkts", "Bytes", "Max σ", "Avg ΔT", "Std ΔT", "Outliers", "Protocols", "Signal Plot"
])
# Configure table for compact, dense display with user-adjustable columns
header = self.flows_table.horizontalHeader()
# Make all columns user-resizable by dragging
header.setSectionResizeMode(QHeaderView.Interactive)
# Auto-resize headers to fit content first
for i in range(self.flows_table.columnCount()):
header.resizeSection(i, header.sectionSizeHint(i))
# Then resize to content for data columns
self.flows_table.resizeColumnsToContents()
# Set minimum widths for important columns and ensure plot column gets priority
header.setMinimumSectionSize(50) # Minimum for any column
self.flows_table.setColumnWidth(2, max(60, self.flows_table.columnWidth(2))) # Pkts
self.flows_table.setColumnWidth(3, max(80, self.flows_table.columnWidth(3))) # Bytes
self.flows_table.setColumnWidth(4, max(60, self.flows_table.columnWidth(4))) # Max σ
self.flows_table.setColumnWidth(7, max(60, self.flows_table.columnWidth(7))) # Outliers
self.flows_table.setColumnWidth(9, max(600, self.flows_table.columnWidth(9))) # Signal Plot gets 600px minimum
# Ensure the table uses the full available width
header.setStretchLastSection(True) # Last column (plots) stretches to fill remaining space
# Increased row height (25% taller: 24px -> 30px) and styling
self.flows_table.setSelectionBehavior(QTableWidget.SelectRows)
self.flows_table.setAlternatingRowColors(True)
self.flows_table.verticalHeader().setDefaultSectionSize(30) # 25% taller rows
self.flows_table.setShowGrid(True)
self.flows_table.setGridStyle(Qt.DotLine) # Subtle grid lines
self.flows_table.itemSelectionChanged.connect(self.on_flow_selected)
# Enable sorting
self.flows_table.setSortingEnabled(True)
# Modern dark theme styling for table
self.flows_table.setStyleSheet("""
QTableWidget {
background-color: #1e1e1e;
color: #ffffff;
font-size: 11px;
gridline-color: #404040;
border: 1px solid #404040;
border-radius: 6px;
selection-background-color: #0078d4;
alternate-background-color: #252525;
}
QTableWidget::item {
padding: 6px 8px;
border: none;
}
QTableWidget::item:selected {
background-color: #0078d4;
color: #ffffff;
}
QTableWidget::item:hover {
background-color: #2d2d2d;
}
QHeaderView::section {
background-color: #2d2d2d;
color: #ffffff;
padding: 8px 12px;
border: none;
border-right: 1px solid #404040;
border-bottom: 1px solid #404040;
font-weight: bold;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
QHeaderView::section:hover {
background-color: #404040;
}
QHeaderView::section:pressed {
background-color: #505050;
}
/* Scrollbar styling */
QScrollBar:vertical {
background-color: #2d2d2d;
width: 12px;
border-radius: 6px;
}
QScrollBar::handle:vertical {
background-color: #505050;
border-radius: 6px;
min-height: 20px;
}
QScrollBar::handle:vertical:hover {
background-color: #606060;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
border: none;
background: none;
}
QScrollBar:horizontal {
background-color: #2d2d2d;
height: 12px;
border-radius: 6px;
}
QScrollBar::handle:horizontal {
background-color: #505050;
border-radius: 6px;
min-width: 20px;
}
QScrollBar::handle:horizontal:hover {
background-color: #606060;
}
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
border: none;
background: none;
}
""")
layout.addWidget(self.flows_table)
# Compact status section
self.status_label = QLabel("Chapter 10 flows (highlighted) show integrated signal plots • All columns are resizable")
self.status_label.setStyleSheet("""
QLabel {
color: #888888;
font-style: italic;
padding: 4px 8px;
font-size: 10px;
background-color: #252525;
border-radius: 4px;
}
""")
self.status_label.setMaximumHeight(25)
layout.addWidget(self.status_label)
self.setLayout(layout)
def set_analyzer(self, analyzer: 'EthernetAnalyzer'):
"""Set the analyzer and populate flow data"""
self.analyzer = analyzer
# Clear cache when analyzer changes
self._signal_cache.clear()
self.populate_flows_table()
def update_file_info(self, file_path: str):
"""Update file information display"""
if not self.analyzer:
return
summary = self.analyzer.get_summary()
file_info = f"<b>File:</b> {os.path.basename(file_path)}<br/>"
file_info += f"<b>Packets:</b> {summary['total_packets']:,}<br/>"
file_info += f"<b>Flows:</b> {summary['unique_flows']}<br/>"
file_info += f"<b>IPs:</b> {summary['unique_ips']}"
self.file_info_label.setText(file_info)
def populate_flows_table(self):
"""Populate the flows table with data"""
if not self.analyzer:
return
summary = self.analyzer.get_summary()
flows_list = list(summary['flows'].values())
# Sort by maximum sigma deviation
flows_list.sort(key=lambda x: (
self.analyzer.statistics_engine.get_max_sigma_deviation(x),
x.frame_count
), reverse=True)
self.flows_table.setRowCount(len(flows_list))
for row, flow in enumerate(flows_list):
# Source IP (column 0)
src_item = QTableWidgetItem(flow.src_ip)
has_ch10 = any('CH10' in ft or 'TMATS' in ft for ft in flow.frame_types.keys())
if has_ch10:
font = QFont()
font.setBold(True)
src_item.setFont(font)
src_item.setBackground(QColor(0, 120, 212, 80)) # Modern blue highlight
src_item.setData(Qt.UserRole, flow) # Store flow object
self.flows_table.setItem(row, 0, src_item)
# Destination IP (column 1)
dst_item = QTableWidgetItem(flow.dst_ip)
if has_ch10:
font = QFont()
font.setBold(True)
dst_item.setFont(font)
dst_item.setBackground(QColor(0, 120, 212, 80)) # Modern blue highlight
self.flows_table.setItem(row, 1, dst_item)
# Packets (column 2)
packets_item = QTableWidgetItem(f"{flow.frame_count:,}")
packets_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.flows_table.setItem(row, 2, packets_item)
# Total Bytes (column 3)
bytes_item = QTableWidgetItem(f"{flow.total_bytes:,}")
bytes_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.flows_table.setItem(row, 3, bytes_item)
# Max sigma deviation (column 4)
max_sigma = self.analyzer.statistics_engine.get_max_sigma_deviation(flow)
sigma_item = QTableWidgetItem(f"{max_sigma:.2f}σ")
sigma_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
# Color code sigma values with modern colors
if max_sigma > 5.0:
sigma_item.setBackground(QColor(220, 53, 69, 120)) # Modern red for high sigma
sigma_item.setForeground(QColor(255, 255, 255)) # White text for contrast
elif max_sigma > 3.0:
sigma_item.setBackground(QColor(255, 193, 7, 120)) # Modern amber for medium sigma
sigma_item.setForeground(QColor(0, 0, 0)) # Black text for contrast
self.flows_table.setItem(row, 4, sigma_item)
# Average inter-arrival time (column 5)
avg_time = f"{flow.avg_inter_arrival:.6f}s" if flow.avg_inter_arrival > 0 else "N/A"
avg_item = QTableWidgetItem(avg_time)
avg_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.flows_table.setItem(row, 5, avg_item)
# Standard deviation (column 6)
std_time = f"{flow.std_inter_arrival:.6f}s" if flow.std_inter_arrival > 0 else "N/A"
std_item = QTableWidgetItem(std_time)
std_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.flows_table.setItem(row, 6, std_item)
# Outlier count (column 7)
outlier_count = len(flow.outlier_frames)
outlier_item = QTableWidgetItem(str(outlier_count))
outlier_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
if outlier_count > 0:
outlier_item.setBackground(QColor(220, 53, 69, 100)) # Modern red for outliers
outlier_item.setForeground(QColor(255, 255, 255)) # White text for contrast
self.flows_table.setItem(row, 7, outlier_item)
# Protocols (column 8) - compact format
protocols = []
if flow.detected_protocol_types:
protocols.extend(flow.detected_protocol_types)
protocols.extend([p for p in flow.protocols if p not in protocols])
protocols_text = ", ".join(protocols[:3]) # Limit to first 3 protocols
if len(protocols) > 3:
protocols_text += f" (+{len(protocols)-3})"
protocols_item = QTableWidgetItem(protocols_text)
self.flows_table.setItem(row, 8, protocols_item)
# Signal plot column (column 9)
if has_ch10:
plot_widget = self._create_integrated_plot_widget(flow, f"{flow.src_ip}{flow.dst_ip}")
self.flows_table.setCellWidget(row, 9, plot_widget)
else:
plot_item = QTableWidgetItem("N/A")
plot_item.setTextAlignment(Qt.AlignCenter)
plot_item.setFlags(plot_item.flags() & ~Qt.ItemIsEditable)
self.flows_table.setItem(row, 9, plot_item)
# Auto-resize numeric columns to fit content
for col in [2, 3, 4, 7]: # Packets, Bytes, Max σ, Outliers
self.flows_table.resizeColumnToContents(col)
def populate_flows_table_quick(self):
"""Populate the flows table quickly without plots for immediate display"""
if not self.analyzer:
return
summary = self.analyzer.get_summary()
flows_list = list(summary['flows'].values())
# Sort by maximum sigma deviation
flows_list.sort(key=lambda x: (
self.analyzer.statistics_engine.get_max_sigma_deviation(x),
x.frame_count
), reverse=True)
self.flows_table.setRowCount(len(flows_list))
for row, flow in enumerate(flows_list):
# Use same logic as populate_flows_table but with placeholder for plots
has_ch10 = any('CH10' in ft or 'TMATS' in ft for ft in flow.frame_types.keys())
# Source IP (column 0)
src_item = QTableWidgetItem(flow.src_ip)
if has_ch10:
font = QFont()
font.setBold(True)
src_item.setFont(font)
src_item.setBackground(QColor(0, 120, 212, 80))
src_item.setData(Qt.UserRole, flow)
self.flows_table.setItem(row, 0, src_item)
# Destination IP (column 1)
dst_item = QTableWidgetItem(flow.dst_ip)
if has_ch10:
font = QFont()
font.setBold(True)
dst_item.setFont(font)
dst_item.setBackground(QColor(0, 120, 212, 80))
self.flows_table.setItem(row, 1, dst_item)
# Fill all other columns as in full populate method
for col, value in [
(2, f"{flow.frame_count:,}"),
(3, f"{flow.total_bytes:,}"),
(4, f"{self.analyzer.statistics_engine.get_max_sigma_deviation(flow):.2f}σ"),
(5, f"{flow.avg_inter_arrival:.6f}s" if flow.avg_inter_arrival > 0 else "N/A"),
(6, f"{flow.std_inter_arrival:.6f}s" if flow.std_inter_arrival > 0 else "N/A"),
(7, str(len(flow.outlier_frames))),
(8, ", ".join(list(flow.detected_protocol_types)[:2] if flow.detected_protocol_types else list(flow.protocols)[:2]))
]:
item = QTableWidgetItem(value)
if col in [2, 3, 4, 5, 6, 7]: # Right-align numeric columns
item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.flows_table.setItem(row, col, item)
# Plot column placeholder
if has_ch10:
plot_item = QTableWidgetItem("Loading plot...")
plot_item.setTextAlignment(Qt.AlignCenter)
plot_item.setForeground(QColor(128, 128, 128))
plot_item.setFlags(plot_item.flags() & ~Qt.ItemIsEditable)
self.flows_table.setItem(row, 9, plot_item)
else:
plot_item = QTableWidgetItem("N/A")
plot_item.setTextAlignment(Qt.AlignCenter)
plot_item.setFlags(plot_item.flags() & ~Qt.ItemIsEditable)
self.flows_table.setItem(row, 9, plot_item)
# Auto-resize numeric columns to fit content
for col in [2, 3, 4, 7]: # Packets, Bytes, Max σ, Outliers
self.flows_table.resizeColumnToContents(col)
def on_flow_selected(self):
"""Handle flow selection"""
selected_rows = self.flows_table.selectionModel().selectedRows()
if not selected_rows:
self.status_label.setText("Chapter 10 flows (highlighted) show integrated signal plots • All columns are resizable")
return
# Get selected flow
row = selected_rows[0].row()
flow_item = self.flows_table.item(row, 0)
flow = flow_item.data(Qt.UserRole)
if flow:
# Check if flow has Chapter 10 data
has_ch10 = any('CH10' in ft or 'TMATS' in ft for ft in flow.frame_types.keys())
if has_ch10:
self.status_label.setText("Chapter 10 flow selected - signal plot shown in table")
else:
self.status_label.setText("Selected flow does not contain Chapter 10 telemetry data")
def _create_integrated_plot_widget(self, flow: 'FlowStats', flow_key: str) -> QWidget:
"""Create a small matplotlib widget for integrated plots"""
if not _ensure_matplotlib_gui_loaded():
placeholder = QLabel("Matplotlib not available")
placeholder.setAlignment(Qt.AlignCenter)
return placeholder
# Create a compact matplotlib widget
plot_widget = QWidget()
layout = QVBoxLayout()
layout.setContentsMargins(2, 2, 2, 2)
# Create wider figure for integrated display with dark theme
figure = Figure(figsize=(8, 2.5), facecolor='#1e1e1e') # Wider and slightly taller
canvas = FigureCanvas(figure)
canvas.setMaximumHeight(150) # Increased height to match taller rows
# Apply dark theme to matplotlib
canvas.setStyleSheet("background-color: #1e1e1e; border: 1px solid #404040; border-radius: 4px;")
layout.addWidget(canvas)
plot_widget.setLayout(layout)
# Load and plot data immediately
self._populate_integrated_plot(figure, canvas, flow, flow_key)
return plot_widget
def _populate_integrated_plot(self, figure, canvas, flow: 'FlowStats', flow_key: str):
"""Populate an integrated plot with actual signal data"""
try:
if not self.analyzer:
self._create_error_plot(figure, canvas, "No analyzer available")
return
# Check cache first
cache_key = f"{flow.src_ip}_{flow.dst_ip}"
if cache_key in self._signal_cache:
signal_data_list = self._signal_cache[cache_key]
else:
# Get flow packets
flow_packets = self._get_flow_packets(flow)
if not flow_packets:
self._create_error_plot(figure, canvas, "No packets found")
return
# Extract signal data using the same method as floating plots
from ..utils.signal_visualizer import signal_visualizer
# Extract TMATS metadata and signals
tmats_metadata = signal_visualizer._extract_tmats_from_flow(flow_packets)
signal_data_list = signal_visualizer._extract_signals_from_flow(flow_packets, tmats_metadata)
# Cache the result
self._signal_cache[cache_key] = signal_data_list
if not signal_data_list:
self._create_error_plot(figure, canvas, "No decodable\nsignal data")
return
# Plot the first signal with dark theme styling
ax = figure.add_subplot(111, facecolor='#1e1e1e')
# Use the first signal for the integrated plot
signal_data = signal_data_list[0]
# Plot the first channel in the signal
if signal_data.channels:
channel_name, data = next(iter(signal_data.channels.items()))
# Downsample if too many points for integrated view
timestamps = signal_data.timestamps
if len(timestamps) > 200:
# Simple downsampling by taking every nth point
step = len(timestamps) // 200
timestamps = timestamps[::step]
data = data[::step]
# Modern color scheme - blue for primary data
ax.plot(timestamps, data, color='#0078d4', linewidth=1.2, alpha=0.9)
# Dark theme styling
ax.set_xlabel('Time (s)', fontsize=7, color='#ffffff')
ax.set_ylabel('Amplitude', fontsize=7, color='#ffffff')
ax.tick_params(labelsize=6, colors='#cccccc')
ax.grid(True, alpha=0.15, color='#404040')
# Style the spines
for spine in ax.spines.values():
spine.set_color('#404040')
spine.set_linewidth(0.5)
# Add compact title with dark theme
title_text = f'{channel_name} (+{len(signal_data_list)-1} more)' if len(signal_data_list) > 1 else channel_name
ax.set_title(title_text, fontsize=7, color='#ffffff', pad=8)
else:
self._create_error_plot(figure, canvas, "No channel data")
return
# Tight layout with minimal horizontal margins (fixed pixel-equivalent values)
figure.subplots_adjust(left=0.08, right=0.98, top=0.85, bottom=0.25)
canvas.draw_idle() # Use draw_idle to avoid window creation
except Exception as e:
self._create_error_plot(figure, canvas, f"Error:\n{str(e)[:30]}...")
def _get_flow_packets(self, flow: 'FlowStats'):
"""Get all packets for a specific flow"""
if not self.analyzer or not hasattr(self.analyzer, 'all_packets') or not self.analyzer.all_packets:
return []
flow_packets = []
for packet in self.analyzer.all_packets:
try:
if hasattr(packet, 'haslayer'):
from scapy.all import IP
if packet.haslayer(IP):
ip_layer = packet[IP]
if ip_layer.src == flow.src_ip and ip_layer.dst == flow.dst_ip:
flow_packets.append(packet)
except:
continue
return flow_packets
def _create_error_plot(self, figure, canvas, message: str):
"""Create an error message plot with dark theme"""
ax = figure.add_subplot(111, facecolor='#1e1e1e')
ax.text(0.5, 0.5, message,
ha='center', va='center', transform=ax.transAxes,
fontsize=8, color='#ff6b6b', style='italic') # Modern red for errors
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.axis('off')
figure.subplots_adjust(left=0, right=1, top=1, bottom=0)
canvas.draw_idle() # Use draw_idle to avoid window creation
class FlowDetailDockWidget(QDockWidget):
"""Dockable widget showing detailed information about selected flow"""
def __init__(self, parent=None):
super().__init__("Flow Details", parent)
# Create main widget
main_widget = QWidget()
layout = QVBoxLayout()
# Detail text area with dark theme
self.detail_text = QTextEdit()
self.detail_text.setReadOnly(True)
self.detail_text.setMaximumHeight(150)
self.detail_text.setStyleSheet("""
QTextEdit {
background-color: #1e1e1e;
color: #ffffff;
border: 1px solid #404040;
border-radius: 6px;
padding: 8px;
font-family: 'Monaco', 'Menlo', 'DejaVu Sans Mono', monospace;
font-size: 11px;
selection-background-color: #0078d4;
}
""")
layout.addWidget(self.detail_text)
main_widget.setLayout(layout)
self.setWidget(main_widget)
# Set dock properties
self.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable)
self.setAllowedAreas(Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
# Initial content with dark theme colors
self.detail_text.setHtml('<span style="color: #888888; font-style: italic;">Select a flow to view detailed information</span>')
def update_flow_details(self, flow: 'FlowStats', analyzer: 'EthernetAnalyzer'):
"""Update the detail view with flow information"""
if not flow or not analyzer:
self.detail_text.setHtml('<span style="color: #888888; font-style: italic;">No flow selected</span>')
return
max_sigma = analyzer.statistics_engine.get_max_sigma_deviation(flow)
html = f"""
<div style="color: #ffffff; font-family: 'Monaco', 'Menlo', 'DejaVu Sans Mono', monospace;">
<h3 style="color: #0078d4; margin-bottom: 12px;">{flow.src_ip}{flow.dst_ip}</h3>
<table border="0" cellpadding="8" cellspacing="0" style="width: 100%; border-collapse: collapse;">
<tr style="background-color: #2d2d2d;"><td style="color: #cccccc; font-weight: bold; border: 1px solid #404040;">Packets:</td><td style="color: #ffffff; border: 1px solid #404040;">{flow.frame_count:,}</td></tr>
<tr style="background-color: #252525;"><td style="color: #cccccc; font-weight: bold; border: 1px solid #404040;">Total Bytes:</td><td style="color: #ffffff; border: 1px solid #404040;">{flow.total_bytes:,}</td></tr>
<tr style="background-color: #2d2d2d;"><td style="color: #cccccc; font-weight: bold; border: 1px solid #404040;">Max Sigma Deviation:</td><td style="color: #ff6b6b; font-weight: bold; border: 1px solid #404040;">{max_sigma:.2f}σ</td></tr>
<tr style="background-color: #252525;"><td style="color: #cccccc; font-weight: bold; border: 1px solid #404040;">Protocols:</td><td style="color: #ffffff; border: 1px solid #404040;">{', '.join(flow.protocols)}</td></tr>
"""
if flow.detected_protocol_types:
html += f'<tr style="background-color: #2d2d2d;"><td style="color: #cccccc; font-weight: bold; border: 1px solid #404040;">Enhanced Protocols:</td><td style="color: #ffffff; border: 1px solid #404040;">{", ".join(flow.detected_protocol_types)}</td></tr>'
if flow.avg_inter_arrival > 0:
html += f"""
<tr style="background-color: #252525;"><td style="color: #cccccc; font-weight: bold; border: 1px solid #404040;">Avg Inter-arrival:</td><td style="color: #ffffff; border: 1px solid #404040;">{flow.avg_inter_arrival:.6f}s</td></tr>
<tr style="background-color: #2d2d2d;"><td style="color: #cccccc; font-weight: bold; border: 1px solid #404040;">Std Deviation:</td><td style="color: #ffffff; border: 1px solid #404040;">{flow.std_inter_arrival:.6f}s</td></tr>
"""
if flow.outlier_frames:
html += f'<tr style="background-color: #252525;"><td style="color: #cccccc; font-weight: bold; border: 1px solid #404040;">Outlier Frames:</td><td style="color: #ff6b6b; font-weight: bold; border: 1px solid #404040;">{len(flow.outlier_frames)}</td></tr>'
html += "</table>"
# Frame type breakdown
if flow.frame_types:
html += '<h4 style="color: #0078d4; margin-top: 16px; margin-bottom: 8px;">Frame Types:</h4>'
html += '<ul style="list-style-type: none; padding-left: 0;">'
for frame_type, ft_stats in flow.frame_types.items():
avg_str = f"{ft_stats.avg_inter_arrival:.3f}s" if ft_stats.avg_inter_arrival > 0 else "N/A"
html += f'<li style="margin-bottom: 4px; padding: 4px 8px; background-color: #252525; border-radius: 4px; border-left: 3px solid #0078d4;"><span style="color: #cccccc; font-weight: bold;">{frame_type}:</span> <span style="color: #ffffff;">{ft_stats.count} packets, avg {avg_str}</span></li>'
html += "</ul>"
html += "</div>"
self.detail_text.setHtml(html)

View File

@@ -4,17 +4,20 @@ Main GUI window for StreamLens
import sys
import os
from typing import Optional, List, TYPE_CHECKING
from typing import Optional, List, TYPE_CHECKING, Dict
try:
from PySide6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QSplitter,
QTableWidget, QTableWidgetItem, QTextEdit, QMenuBar, QMenu,
QFileDialog, QMessageBox, QProgressBar, QStatusBar, QLabel,
QHeaderView, QPushButton, QGroupBox
QHeaderView, QPushButton, QGroupBox, QDockWidget, QToolBar,
QButtonGroup, QComboBox, QSpinBox
)
from PySide6.QtCore import Qt, QThread, Signal, QTimer
from PySide6.QtGui import QAction, QIcon, QFont
from PySide6.QtGui import QAction, QIcon, QFont, QKeySequence, QActionGroup
from .dock_panels import FlowListDockWidget, FlowDetailDockWidget
# Matplotlib integration - lazy loaded
matplotlib = None
@@ -40,6 +43,21 @@ try:
from matplotlib.figure import Figure as Fig
import matplotlib.pyplot as pyplot
# Turn off interactive mode to prevent floating windows
pyplot.ioff()
# Ensure no figure windows are created
mpl.rcParams['figure.max_open_warning'] = 0
mpl.rcParams['backend'] = 'Qt5Agg'
# Prevent any automatic figure display
mpl.pyplot.show = lambda *args, **kwargs: None
pyplot.show = lambda *args, **kwargs: None
# Also prevent figure.show()
original_figure_show = Fig.show
Fig.show = lambda self, *args, **kwargs: None
FigureCanvas = FC
NavigationToolbar = NT
Figure = Fig
@@ -59,103 +77,6 @@ if TYPE_CHECKING:
from ..models.flow_stats import FlowStats
class PlotWidget(QWidget):
"""Widget containing matplotlib plot with toolbar"""
def __init__(self, parent=None):
super().__init__(parent)
self.figure = None
self.canvas = None
self.toolbar = None
self._initialized = False
# Just create basic layout initially
self.layout = QVBoxLayout()
self.setLayout(self.layout)
def _ensure_initialized(self):
"""Initialize matplotlib components when first needed"""
if self._initialized:
return True
if not _ensure_matplotlib_gui_loaded():
return False
self.figure = Figure(figsize=(12, 8))
self.canvas = FigureCanvas(self.figure)
self.toolbar = NavigationToolbar(self.canvas, self)
self.layout.addWidget(self.toolbar)
self.layout.addWidget(self.canvas)
self._initialized = True
self.clear_plot()
return True
def clear_plot(self):
"""Clear the plot and show initial message"""
if not self._ensure_initialized():
return
self.figure.clear()
# Add a centered message when no plot is shown
ax = self.figure.add_subplot(111)
ax.text(0.5, 0.5, 'Select a flow with Chapter 10 data to view signal visualization',
ha='center', va='center', transform=ax.transAxes, fontsize=12,
color='gray', style='italic')
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.axis('off')
self.canvas.draw()
def plot_flow_signals(self, flow: 'FlowStats', signal_data_list: List, flow_key: str):
"""Plot Chapter 10 signals for a flow"""
if not self._ensure_initialized():
return
self.figure.clear()
if not signal_data_list:
# Show message when no data
ax = self.figure.add_subplot(111)
ax.text(0.5, 0.5, 'No Chapter 10 signal data found in selected flow',
ha='center', va='center', transform=ax.transAxes, fontsize=14)
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.axis('off')
self.canvas.draw()
return
# Create subplots for each signal
n_signals = len(signal_data_list)
axes = []
for idx, signal_data in enumerate(signal_data_list):
ax = self.figure.add_subplot(n_signals, 1, idx + 1)
axes.append(ax)
# Plot each channel in the signal data
for channel_name, data in signal_data.channels.items():
ax.plot(signal_data.timestamps, data, label=channel_name, linewidth=0.8)
ax.set_xlabel('Time (s)')
ax.set_ylabel('Amplitude')
ax.grid(True, alpha=0.3)
ax.legend()
# Add metadata info
if signal_data.metadata and signal_data.metadata.channel_configs:
config_info = []
for ch_id, config in signal_data.metadata.channel_configs.items():
if 'units' in config:
config_info.append(f"CH{ch_id}: {config.get('units', 'Unknown')}")
if config_info:
ax.set_title(f"Channels: {', '.join(config_info)}")
self.figure.suptitle(f'Chapter 10 Signals - Flow: {flow_key}', fontsize=14)
self.figure.tight_layout()
self.canvas.draw()
class PCAPLoadThread(QThread):
"""Thread for loading PCAP files without blocking UI"""
@@ -204,136 +125,319 @@ class PCAPLoadThread(QThread):
self.error_occurred.emit(str(e))
class StreamLensMainWindow(QMainWindow):
"""Main application window"""
"""Main application window with docking panels"""
def __init__(self):
super().__init__()
self.analyzer: Optional['EthernetAnalyzer'] = None
self.current_file = None
self.loading_thread = None
self._initializing = True # Flag to prevent early event triggers
self.setWindowTitle("StreamLens - Ethernet Traffic Analyzer")
self.setGeometry(100, 100, 1400, 900)
self.setGeometry(100, 100, 1600, 1000) # Larger default size for docking
# Apply modern dark theme
self.apply_dark_theme()
self.setup_ui()
self.setup_menus()
self.setup_toolbar()
self.setup_docking_ui()
self.setup_status_bar()
def setup_ui(self):
"""Set up the main UI layout"""
central_widget = QWidget()
self.setCentralWidget(central_widget)
# Create main horizontal splitter
main_splitter = QSplitter(Qt.Horizontal)
# Left panel - Flow list and controls
left_panel = self.create_left_panel()
left_panel.setMaximumWidth(600)
left_panel.setMinimumWidth(400)
# Right panel - Plot area
right_panel = self.create_right_panel()
main_splitter.addWidget(left_panel)
main_splitter.addWidget(right_panel)
main_splitter.setStretchFactor(0, 0) # Don't stretch left panel
main_splitter.setStretchFactor(1, 1) # Stretch right panel
layout = QHBoxLayout()
layout.addWidget(main_splitter)
central_widget.setLayout(layout)
# Initialization complete
self._initializing = False
def create_left_panel(self):
"""Create the left panel with flow list and info"""
widget = QWidget()
layout = QVBoxLayout()
def apply_dark_theme(self):
"""Apply modern dark theme to the entire application"""
dark_stylesheet = """
/* Main Window Dark Theme */
QMainWindow {
background-color: #1e1e1e;
color: #ffffff;
}
# File info group
file_group = QGroupBox("File Information")
file_layout = QVBoxLayout()
self.file_info_label = QLabel("No file loaded")
self.file_info_label.setWordWrap(True)
file_layout.addWidget(self.file_info_label)
file_group.setLayout(file_layout)
layout.addWidget(file_group)
/* Menu Bar */
QMenuBar {
background-color: #2d2d2d;
color: #ffffff;
border-bottom: 1px solid #404040;
padding: 2px;
}
# Flow list group
flows_group = QGroupBox("IP Flows (sorted by max sigma deviation)")
flows_layout = QVBoxLayout()
QMenuBar::item {
background-color: transparent;
padding: 8px 12px;
border-radius: 4px;
}
self.flows_table = QTableWidget()
self.flows_table.setColumnCount(5)
self.flows_table.setHorizontalHeaderLabels([
"Source → Destination", "Packets", "Max σ", "Protocols", "Frame Types"
])
QMenuBar::item:selected {
background-color: #404040;
}
# Configure table
header = self.flows_table.horizontalHeader()
header.setStretchLastSection(True)
header.setSectionResizeMode(0, QHeaderView.Stretch)
QMenuBar::item:pressed {
background-color: #505050;
}
self.flows_table.setSelectionBehavior(QTableWidget.SelectRows)
self.flows_table.setAlternatingRowColors(True)
self.flows_table.itemSelectionChanged.connect(self.on_flow_selected)
/* Menu Dropdown */
QMenu {
background-color: #2d2d2d;
color: #ffffff;
border: 1px solid #404040;
border-radius: 6px;
padding: 4px;
}
flows_layout.addWidget(self.flows_table)
QMenu::item {
padding: 8px 20px;
border-radius: 4px;
}
# Status label for visualization feedback
self.viz_status_label = QLabel("Select a flow to view signal visualization")
self.viz_status_label.setStyleSheet("color: gray; font-style: italic;")
flows_layout.addWidget(self.viz_status_label)
QMenu::item:selected {
background-color: #404040;
}
flows_group.setLayout(flows_layout)
layout.addWidget(flows_group)
/* Toolbar */
QToolBar {
background-color: #2d2d2d;
border: none;
padding: 4px;
spacing: 8px;
}
widget.setLayout(layout)
return widget
QToolButton {
background-color: #404040;
color: #ffffff;
border: 1px solid #505050;
border-radius: 6px;
padding: 8px 12px;
font-weight: 500;
}
QToolButton:hover {
background-color: #505050;
border-color: #606060;
}
QToolButton:pressed {
background-color: #353535;
}
/* Status Bar */
QStatusBar {
background-color: #2d2d2d;
color: #cccccc;
border-top: 1px solid #404040;
padding: 4px;
}
/* Progress Bar */
QProgressBar {
background-color: #404040;
border: 1px solid #505050;
border-radius: 4px;
text-align: center;
color: #ffffff;
}
QProgressBar::chunk {
background-color: #0078d4;
border-radius: 3px;
}
/* Spin Box */
QSpinBox {
background-color: #404040;
color: #ffffff;
border: 1px solid #505050;
border-radius: 4px;
padding: 4px 8px;
min-width: 50px;
}
QSpinBox:hover {
border-color: #606060;
}
QSpinBox:focus {
border-color: #0078d4;
}
QSpinBox::up-button, QSpinBox::down-button {
background-color: #505050;
border: none;
width: 16px;
}
QSpinBox::up-button:hover, QSpinBox::down-button:hover {
background-color: #606060;
}
/* Labels */
QLabel {
color: #ffffff;
}
/* Dock Widgets */
QDockWidget {
background-color: #1e1e1e;
color: #ffffff;
titlebar-close-icon: none;
titlebar-normal-icon: none;
}
QDockWidget::title {
background-color: #2d2d2d;
color: #ffffff;
padding: 8px;
border-bottom: 1px solid #404040;
font-weight: bold;
}
/* Splitter */
QSplitter::handle {
background-color: #404040;
}
QSplitter::handle:horizontal {
width: 2px;
}
QSplitter::handle:vertical {
height: 2px;
}
"""
self.setStyleSheet(dark_stylesheet)
def setup_docking_ui(self):
"""Set up the UI layout with integrated plots"""
# Create docking panels
self.flow_list_dock = FlowListDockWidget(self)
self.flow_detail_dock = FlowDetailDockWidget(self)
# Set flow list as central widget to take full width
self.setCentralWidget(self.flow_list_dock)
# Add detail dock at bottom
self.addDockWidget(Qt.BottomDockWidgetArea, self.flow_detail_dock)
# Connect signals
self.flow_list_dock.flows_table.itemSelectionChanged.connect(self.on_flow_selection_changed)
# Set detail dock height
self.resizeDocks([self.flow_detail_dock], [150], Qt.Vertical)
def create_right_panel(self):
"""Create the right panel with plot area"""
widget = QWidget()
layout = QVBoxLayout()
def on_flow_selection_changed(self):
"""Handle flow selection change to update detail panel"""
if not hasattr(self, 'flow_list_dock') or not self.analyzer:
return
selected_rows = self.flow_list_dock.flows_table.selectionModel().selectedRows()
plot_group = QGroupBox("Signal Visualization")
plot_layout = QVBoxLayout()
if not selected_rows:
self.flow_detail_dock.update_flow_details(None, None)
return
self.plot_widget = PlotWidget()
plot_layout.addWidget(self.plot_widget)
# Get selected flow
row = selected_rows[0].row()
flow_item = self.flow_list_dock.flows_table.item(row, 0)
flow = flow_item.data(Qt.UserRole)
plot_group.setLayout(plot_layout)
layout.addWidget(plot_group)
widget.setLayout(layout)
return widget
if flow:
self.flow_detail_dock.update_flow_details(flow, self.analyzer)
def setup_menus(self):
"""Set up application menus"""
menubar = self.menuBar()
# File menu
file_menu = menubar.addMenu("File")
file_menu = menubar.addMenu("&File")
open_action = QAction("Open PCAP...", self)
open_action.setShortcut("Ctrl+O")
# Open PCAP
open_action = QAction("&Open PCAP...", self)
open_action.setShortcut(QKeySequence.Open)
open_action.setStatusTip("Open a PCAP file for analysis")
open_action.triggered.connect(self.open_pcap_file)
file_menu.addAction(open_action)
# Monitor NIC
monitor_action = QAction("&Monitor NIC...", self)
monitor_action.setShortcut("Ctrl+M")
monitor_action.setStatusTip("Start live network monitoring")
monitor_action.triggered.connect(self.monitor_nic)
file_menu.addAction(monitor_action)
file_menu.addSeparator()
exit_action = QAction("Exit", self)
exit_action.setShortcut("Ctrl+Q")
# Exit
exit_action = QAction("E&xit", self)
exit_action.setShortcut(QKeySequence.Quit)
exit_action.setStatusTip("Exit StreamLens")
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# View menu
view_menu = menubar.addMenu("View")
view_menu = menubar.addMenu("&View")
refresh_action = QAction("Refresh", self)
refresh_action.setShortcut("F5")
# Refresh
refresh_action = QAction("&Refresh", self)
refresh_action.setShortcut(QKeySequence.Refresh)
refresh_action.setStatusTip("Refresh current data")
refresh_action.triggered.connect(self.refresh_data)
view_menu.addAction(refresh_action)
view_menu.addSeparator()
# Show/Hide panels
show_flow_details_action = QAction("Show Flow &Details", self)
show_flow_details_action.setCheckable(True)
show_flow_details_action.setChecked(True)
show_flow_details_action.triggered.connect(lambda checked: self.flow_detail_dock.setVisible(checked))
view_menu.addAction(show_flow_details_action)
# Help menu
help_menu = menubar.addMenu("&Help")
about_action = QAction("&About StreamLens...", self)
about_action.setStatusTip("About StreamLens")
about_action.triggered.connect(self.show_about)
help_menu.addAction(about_action)
def setup_toolbar(self):
"""Set up application toolbar"""
toolbar = QToolBar("Main Toolbar")
toolbar.setMovable(False)
self.addToolBar(toolbar)
# File operations
open_action = QAction("Open", self)
open_action.setShortcut(QKeySequence.Open)
open_action.setStatusTip("Open PCAP file")
open_action.triggered.connect(self.open_pcap_file)
toolbar.addAction(open_action)
monitor_action = QAction("Monitor", self)
monitor_action.setStatusTip("Monitor network interface")
monitor_action.triggered.connect(self.monitor_nic)
toolbar.addAction(monitor_action)
toolbar.addSeparator()
# Outlier threshold control
toolbar.addWidget(QLabel("Outlier Threshold: "))
self.outlier_threshold_spin = QSpinBox()
self.outlier_threshold_spin.setRange(1, 10)
self.outlier_threshold_spin.setValue(3)
self.outlier_threshold_spin.setSuffix("σ")
self.outlier_threshold_spin.setStatusTip("Set sigma threshold for outlier detection")
self.outlier_threshold_spin.valueChanged.connect(self.on_outlier_threshold_changed)
toolbar.addWidget(self.outlier_threshold_spin)
def setup_status_bar(self):
"""Set up status bar"""
@@ -369,7 +473,7 @@ class StreamLensMainWindow(QMainWindow):
self.progress_bar.setValue(0)
# Disable UI during loading
self.flows_table.setEnabled(False)
self.flow_list_dock.flows_table.setEnabled(False)
# Start loading thread
self.loading_thread = PCAPLoadThread(file_path)
@@ -382,157 +486,27 @@ class StreamLensMainWindow(QMainWindow):
"""Handle PCAP loading completion"""
self.analyzer = analyzer
self.progress_bar.setVisible(False)
self.flows_table.setEnabled(True)
self.flow_list_dock.flows_table.setEnabled(True)
# Update flow list dock with analyzer and file info
self.flow_list_dock.set_analyzer(analyzer)
self.flow_list_dock.update_file_info(self.current_file)
# Populate flows table with embedded plots (synchronous)
self.flow_list_dock.populate_flows_table()
# Update file info
summary = analyzer.get_summary()
file_info = f"File: {os.path.basename(self.current_file)}\n"
file_info += f"Packets: {summary['total_packets']:,}\n"
file_info += f"Flows: {summary['unique_flows']}\n"
file_info += f"IPs: {summary['unique_ips']}"
self.file_info_label.setText(file_info)
# Populate flows table
self.populate_flows_table()
self.status_bar.showMessage(f"Loaded {summary['total_packets']:,} packets from {os.path.basename(self.current_file)}")
self.status_bar.showMessage(f"Loaded {summary['total_packets']:,} packets with embedded plots")
def on_loading_error(self, error_message: str):
"""Handle loading error"""
self.progress_bar.setVisible(False)
self.flows_table.setEnabled(True)
self.flow_list_dock.flows_table.setEnabled(True)
QMessageBox.critical(self, "Loading Error", f"Error loading PCAP file:\n{error_message}")
self.status_bar.showMessage("Error loading file")
def populate_flows_table(self):
"""Populate the flows table with data"""
if not self.analyzer:
return
summary = self.analyzer.get_summary()
flows_list = list(summary['flows'].values())
# Sort by maximum sigma deviation
flows_list.sort(key=lambda x: (
self.analyzer.statistics_engine.get_max_sigma_deviation(x),
x.frame_count
), reverse=True)
self.flows_table.setRowCount(len(flows_list))
for row, flow in enumerate(flows_list):
# Source -> Destination
flow_item = QTableWidgetItem(f"{flow.src_ip}{flow.dst_ip}")
self.flows_table.setItem(row, 0, flow_item)
# Packets
packets_item = QTableWidgetItem(str(flow.frame_count))
packets_item.setData(Qt.UserRole, flow) # Store flow object
self.flows_table.setItem(row, 1, packets_item)
# Max sigma deviation
max_sigma = self.analyzer.statistics_engine.get_max_sigma_deviation(flow)
sigma_item = QTableWidgetItem(f"{max_sigma:.2f}σ")
self.flows_table.setItem(row, 2, sigma_item)
# Protocols
protocols = ", ".join(flow.protocols)
if flow.detected_protocol_types:
protocols += f" ({', '.join(flow.detected_protocol_types)})"
protocols_item = QTableWidgetItem(protocols)
self.flows_table.setItem(row, 3, protocols_item)
# Frame types
frame_types = ", ".join(flow.frame_types.keys())
frame_types_item = QTableWidgetItem(frame_types)
self.flows_table.setItem(row, 4, frame_types_item)
# Resize columns to content
self.flows_table.resizeColumnsToContents()
def on_flow_selected(self):
"""Handle flow selection and automatically render plots"""
selected_rows = self.flows_table.selectionModel().selectedRows()
if not selected_rows:
# No selection - clear plot and show message
self.plot_widget.clear_plot()
self.viz_status_label.setText("Select a flow to view signal visualization")
self.viz_status_label.setStyleSheet("color: gray; font-style: italic;")
return
# Get selected flow and automatically visualize
self.visualize_selected_flow()
def visualize_selected_flow(self):
"""Visualize the selected flow"""
if not self.analyzer:
return
selected_rows = self.flows_table.selectionModel().selectedRows()
if not selected_rows:
return
# Get selected flow
row = selected_rows[0].row()
flow_item = self.flows_table.item(row, 1) # Packets column has flow data
flow = flow_item.data(Qt.UserRole)
if not flow:
return
# Check if flow has Chapter 10 data
has_ch10 = any('CH10' in ft or 'TMATS' in ft for ft in flow.frame_types.keys())
if not has_ch10:
# Clear plot and show informative message
self.plot_widget.clear_plot()
self.viz_status_label.setText("Selected flow does not contain Chapter 10 telemetry data")
self.viz_status_label.setStyleSheet("color: orange; font-style: italic;")
return
# Get flow packets
flow_packets = self.get_flow_packets(flow)
if not flow_packets:
# Clear plot and show error message
self.plot_widget.clear_plot()
self.viz_status_label.setText("No packets found for selected flow")
self.viz_status_label.setStyleSheet("color: red; font-style: italic;")
return
# Use signal visualizer to extract and process signals
from ..utils.signal_visualizer import signal_visualizer
flow_key = f"{flow.src_ip}{flow.dst_ip}"
self.status_bar.showMessage(f"Extracting signals from flow {flow_key}...")
self.viz_status_label.setText("Processing Chapter 10 telemetry data...")
self.viz_status_label.setStyleSheet("color: blue; font-style: italic;")
try:
# Extract signals
tmats_metadata = signal_visualizer._extract_tmats_from_flow(flow_packets)
signal_data_list = signal_visualizer._extract_signals_from_flow(flow_packets, tmats_metadata)
# Plot in GUI
self.plot_widget.plot_flow_signals(flow, signal_data_list, flow_key)
if signal_data_list:
self.status_bar.showMessage(f"Plotted {len(signal_data_list)} signal(s) from {flow_key}")
self.viz_status_label.setText(f"Displaying {len(signal_data_list)} signal channel(s)")
self.viz_status_label.setStyleSheet("color: green; font-weight: bold;")
else:
self.status_bar.showMessage(f"No decodable signal data found in {flow_key}")
self.viz_status_label.setText("No decodable signal data found in selected flow")
self.viz_status_label.setStyleSheet("color: orange; font-style: italic;")
except Exception as e:
# Show error in status label instead of popup
self.plot_widget.clear_plot()
self.viz_status_label.setText(f"Visualization error: {str(e)}")
self.viz_status_label.setStyleSheet("color: red; font-style: italic;")
self.status_bar.showMessage("Visualization error")
def get_flow_packets(self, flow: 'FlowStats') -> List:
"""Get all packets for a specific flow"""
@@ -565,4 +539,48 @@ class StreamLensMainWindow(QMainWindow):
self.loading_thread.quit()
self.loading_thread.wait()
event.accept()
event.accept()
# Menu and toolbar action handlers
def monitor_nic(self):
"""Start network interface monitoring"""
# TODO: Implement NIC monitoring dialog
QMessageBox.information(
self,
"Monitor NIC",
"Network interface monitoring will be implemented in a future version."
)
def show_about(self):
"""Show about dialog"""
QMessageBox.about(
self,
"About StreamLens",
"""<h2>StreamLens v1.0</h2>
<p>Advanced Ethernet Traffic Analyzer</p>
<p>Specialized protocol dissection for aviation and industrial networks
with sigma-based outlier identification and interactive signal visualization.</p>
<p><b>Features:</b></p>
<ul>
<li>Chapter 10 (IRIG 106) telemetry protocol support</li>
<li>PTP (IEEE 1588) and IENA protocol dissection</li>
<li>Statistical outlier detection</li>
<li>Interactive signal visualization</li>
<li>Docking panel interface</li>
</ul>
<p>Built with PySide6 and matplotlib</p>"""
)
def on_outlier_threshold_changed(self, value: int):
"""Handle outlier threshold change"""
if self.analyzer:
# Update analyzer's outlier threshold
self.analyzer.statistics_engine.sigma_threshold = float(value)
# Refresh the flow data with new threshold
if hasattr(self, 'flow_list_dock'):
self.flow_list_dock.populate_flows_table()
self.status_bar.showMessage(f"Outlier threshold updated to {value}σ")

View File

@@ -288,7 +288,9 @@ class Chapter10SignalDecoder:
data_array = data_array * gain + offset
# Generate timestamps (would be more sophisticated in real implementation)
sample_rate = self.tmats_metadata.sample_rate if self.tmats_metadata else 1000.0
sample_rate = 1000.0 # Default sample rate
if self.tmats_metadata and self.tmats_metadata.sample_rate and self.tmats_metadata.sample_rate > 0:
sample_rate = self.tmats_metadata.sample_rate
timestamps = np.arange(len(data_array)) / sample_rate
channel_name = f"CH{channel_id}"
@@ -320,7 +322,10 @@ class Chapter10SignalDecoder:
data_array = np.array(samples, dtype=np.float32)
sample_rate = self.tmats_metadata.sample_rate if self.tmats_metadata else 1000.0
# Use default sample rate if TMATS doesn't provide one
sample_rate = 1000.0 # Default sample rate
if self.tmats_metadata and self.tmats_metadata.sample_rate and self.tmats_metadata.sample_rate > 0:
sample_rate = self.tmats_metadata.sample_rate
timestamps = np.arange(len(data_array)) / sample_rate
channel_name = f"PCM_CH{channel_id}"
@@ -350,17 +355,16 @@ class SignalVisualizer:
def visualize_flow_signals(self, flow: 'FlowStats', packets: List['Packet'], gui_mode: bool = False) -> None:
"""Visualize signals from a Chapter 10 flow"""
# Lazy load matplotlib with appropriate backend
# IMPORTANT: For GUI mode with embedded plots, this method should NOT be called
# Embedded plots should use the _extract methods directly
if gui_mode:
# For GUI mode, use Qt backend for embedded plots
if not _ensure_matplotlib_loaded('Qt5Agg'):
print("Matplotlib not available - cannot visualize signals")
return
else:
# For TUI mode, use Agg backend to avoid GUI windows
if not _ensure_matplotlib_loaded():
print("Matplotlib not available - cannot visualize signals")
return
print("WARNING: visualize_flow_signals called in GUI mode - should use embedded plots instead")
return # Don't create floating windows in GUI mode
# For TUI mode, use Agg backend to avoid GUI windows
if not _ensure_matplotlib_loaded():
print("Matplotlib not available - cannot visualize signals")
return
flow_key = f"{flow.src_ip}->{flow.dst_ip}"
@@ -482,7 +486,9 @@ class SignalVisualizer:
else:
# Subsequent signals - add time offset to create continuous timeline
if len(all_timestamps) > 0:
time_offset = all_timestamps[-1] + (1.0 / signal_data.sample_rate)
# Use safe sample rate (avoid division by None or zero)
safe_sample_rate = signal_data.sample_rate if signal_data.sample_rate and signal_data.sample_rate > 0 else 1000.0
time_offset = all_timestamps[-1] + (1.0 / safe_sample_rate)
# Add offset timestamps
offset_timestamps = signal_data.timestamps + time_offset
@@ -596,9 +602,10 @@ class SignalVisualizer:
print(f"Signal plot saved to {filename}")
plt.close(fig)
else:
# Store reference and show interactively (GUI mode)
# Store reference but DO NOT show for GUI mode embedded plots
# GUI mode should only use embedded widgets, not floating windows
self.active_windows[flow_key] = fig
plt.show()
# Do not call plt.show() - this should only be used for TUI mode file output
except Exception as e:
print(f"Signal visualization error: {e}")