dark compact theme
This commit is contained in:
@@ -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}σ")
|
||||
Reference in New Issue
Block a user