diff --git a/.DS_Store b/.DS_Store
index 979c919..c6de4f1 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/README.md b/README.md
index 6a87bc6..f02cfe3 100644
--- a/README.md
+++ b/README.md
@@ -44,15 +44,23 @@ python streamlens.py --live --filter "port 319 or port 320"
## Features
-### 🖥️ Modern GUI Interface (New!)
-- **Professional Qt Interface**: Cross-platform GUI built with PySide6
-- **Interactive Flow List**: Sortable table showing flows with sigma deviations, protocols, and frame types
-- **Automatic Plot Rendering**: Click any flow to instantly view signal plots (no button needed)
-- **Embedded Matplotlib Plots**: Interactive signal visualization with zoom, pan, and navigation toolbar
+### 🖥️ Modern Dark-Themed GUI Interface with Optimized Layout
+- **Professional Dark Theme**: Modern color palette with #1e1e1e backgrounds and optimized contrast
+- **Content-Fitted Columns**: Headers automatically resize to fit content, not wider than necessary
+- **Full-Width Utilization**: Grid view uses entire screen width with prioritized wide signal plots
+- **Optimized Row Height**: 25% taller rows (30px) for better visual balance and plot visibility
+- **Wide Embedded Plots**: 8x2.5 figure size with minimal horizontal margins for maximum signal detail
+- **Intelligent Column Sizing**: Auto-resizes to content with smart minimums and plot column priority
+- **Professional Qt Interface**: Cross-platform GUI built with PySide6 with native look and feel
+- **Embedded Signal Plots**: Chapter 10 signal plots rendered directly in the flow table cells
+- **Synchronous Plot Rendering**: Plots appear immediately when table loads, no background threads
+- **Chapter 10 Flow Highlighting**: Flows with Chapter 10 data are highlighted in modern blue and bold
+- **Smart Signal Caching**: Avoids repeated processing of the same flow's signal data
+- **Flow Detail Panel**: Dockable bottom panel with dark theme styling
- **Background PCAP Loading**: Progress bar with non-blocking file processing
-- **File Management**: Open PCAP files via dialog or command line
-- **Smart Status Feedback**: Color-coded status messages for different flow types and states
-- **Threading Safety**: Proper Qt threading eliminates segmentation faults
+- **Outlier Threshold Control**: Real-time adjustment of sigma-based outlier detection
+- **Threading Safety**: Main-thread plot creation eliminates Qt threading violations
+- **No Floating Windows**: All plots stay embedded in the grid interface
### Enhanced TUI Interface
- **Three-Panel Layout**: Flows list (top-left), flow details (top-right), timing visualization (bottom)
@@ -76,15 +84,18 @@ python streamlens.py --live --filter "port 319 or port 320"
- **PTP (IEEE 1588-2019)**: Precision Time Protocol message parsing with sync, delay, and announce messages
- **IENA (Airbus)**: Industrial Ethernet Network Architecture with P/D/N/M/Q message types
-### 📊 Chapter 10 Signal Visualization
-- **Interactive GUI Plots**: Select any flow to automatically view embedded matplotlib plots
+### 📊 Chapter 10 Signal Visualization with Dark Theme Integration
+- **Wide Embedded GUI Plots**: Chapter 10 flows display matplotlib plots directly in flow table with 8x2.5 sizing
+- **Dark Theme Plot Integration**: Plots use #1e1e1e backgrounds with white text and modern #0078d4 signal colors
+- **Optimized Plot Margins**: Minimal horizontal margins (8% left, 98% right) for maximum signal visualization area
- **TUI Signal Plots**: Press `v` in the TUI to generate signal files (threading-safe)
- **Signal Consolidation**: Automatically combines multiple packets from the same channel into continuous signals
- **TMATS Integration**: Automatically extracts channel metadata from TMATS frames for proper signal scaling
- **Multi-channel Support**: Displays multiple channels with proper engineering units and scaling
-- **Threading Safety**: GUI uses proper Qt integration, TUI saves plots to files to avoid segfaults
+- **Threading Safety**: GUI uses main-thread plot creation, TUI saves plots to files to avoid segfaults
+- **No Floating Windows**: All GUI plots stay embedded in the table interface
- **Both Modes**: Works for both PCAP analysis and live capture
-- **Matplotlib Features**: Full zoom, pan, save, and navigation capabilities
+- **Enhanced Visual Quality**: 150px plot height with professional styling and grid overlays
### Protocol Detection & Fallbacks
- Automatic protocol identification based on port numbers and packet structure
@@ -122,16 +133,22 @@ Generate detailed outlier reports with `--report` flag showing frame-by-frame si
## GUI Usage
### Main Interface
-- **Left Panel**: File information and flow list sorted by sigma deviation
-- **Right Panel**: Interactive matplotlib plot area with navigation toolbar
+- **Menu Bar**: File operations (Open PCAP, Monitor NIC), View controls, Help system
+- **Toolbar**: File operations and outlier threshold adjustment
+- **Central Flow Table**: Full-width table with file info, flow data, and integrated signal plots
+- **Flow Detail Panel**: Dockable bottom panel showing comprehensive flow information
- **Status Bar**: Loading progress and operation feedback
### Workflow
-1. **Launch GUI**: `python streamlens.py --gui`
-2. **Open PCAP**: File → Open PCAP... or use command line `--pcap` flag
-3. **Select Flow**: Click on any flow in the table to automatically view signal plots
-4. **Interact**: Use matplotlib toolbar to zoom, pan, save plots
-5. **Navigate**: Click different flows to instantly see their signal visualizations
+1. **Launch GUI with PCAP**: `python streamlens.py --gui --pcap file.pcap` (recommended)
+2. **Alternative Launch**: `python streamlens.py --gui`, then File → Open PCAP...
+3. **Immediate Analysis**: Flow table displays instantly with all flow data and wide embedded plots
+4. **Optimized Display**: Content-fitted columns, 25% taller rows, and full-width utilization
+5. **Wide Plot Visualization**: Chapter 10 flows show detailed signal plots with minimal margins
+6. **Browse Flows**: View flows in the dark-themed table (Chapter 10 flows highlighted in modern blue)
+7. **Analyze Details**: Select flows to view detailed information in the dark-themed bottom panel
+8. **Adjust Threshold**: Use toolbar spinner to change outlier detection sensitivity
+9. **Multi-Flow Comparison**: Compare signals across different flows in the same optimized view
## TUI Controls
@@ -172,9 +189,10 @@ streamlens/
│ │ ├── ptp.py # IEEE 1588 Precision Time Protocol
│ │ ├── iena.py # Airbus IENA protocol
│ │ └── standard.py # Standard protocol detection
-│ ├── gui/ # Modern GUI Interface (NEW!)
+│ ├── gui/ # Modern GUI Interface with Docking Panels
│ │ ├── __init__.py # GUI package initialization
-│ │ └── main_window.py # PySide6 main window with matplotlib integration
+│ │ ├── main_window.py # PySide6 main window with docking system
+│ │ └── dock_panels.py # Dockable panel implementations (flow list, plots, details)
│ ├── tui/ # Text User Interface
│ │ ├── interface.py # Main TUI controller
│ │ ├── navigation.py # Navigation handling
diff --git a/ai.comprehensive_replay.md b/ai.comprehensive_replay.md
index 09fe198..9d15dd0 100644
--- a/ai.comprehensive_replay.md
+++ b/ai.comprehensive_replay.md
@@ -26,8 +26,9 @@ Build a sophisticated Python-based network traffic analysis tool called "StreamL
- **High Jitter Detection**: Coefficient of variation analysis for identifying problematic flows
- **Configurable Analysis**: Adjustable outlier thresholds and analysis parameters
- **Chapter 10 Signal Visualization**: Real-time matplotlib-based signal plotting with TMATS integration
-- **Interactive Signal Analysis**: Press 'v' in TUI to generate signal files, or use GUI for embedded interactive plots
-- **Threading-Safe Visualization**: Proper Qt integration for GUI, file output for TUI to prevent segmentation faults
+- **Interactive Signal Analysis**: Press 'v' in TUI to generate signal files, or view embedded plots in GUI table
+- **Threading-Safe Visualization**: Main-thread plot creation for GUI, file output for TUI to prevent segmentation faults
+- **Embedded Plot Integration**: Chapter 10 flows display signal plots directly in the flow table cells
- **Cross-Platform GUI**: PySide6-based interface with file dialogs, progress bars, and embedded matplotlib widgets
## Architecture Overview
@@ -243,9 +244,10 @@ streamlens/
│ │ ├── ptp.py # PTPDissector (IEEE 1588)
│ │ ├── iena.py # IENADissector (Airbus)
│ │ └── standard.py # StandardProtocolDissector
-│ ├── gui/ # Modern GUI Interface system (NEW!)
+│ ├── gui/ # Modern GUI Interface system with Embedded Plots
│ │ ├── __init__.py # GUI package init
-│ │ └── main_window.py # StreamLensMainWindow with PySide6 and matplotlib
+│ │ ├── main_window.py # StreamLensMainWindow with PySide6 and docking system
+│ │ └── dock_panels.py # Dockable panel implementations (flow list, plots, details)
│ ├── tui/ # Text User Interface system
│ │ ├── __init__.py # TUI package init
│ │ ├── interface.py # TUIInterface main controller
@@ -369,34 +371,64 @@ class SignalVisualizer:
self._create_signal_window(flow_key, signal_data, flow)
```
-### 7. PySide6 GUI Architecture with Threading Safety
+### 7. PySide6 GUI Architecture with Embedded Plot Integration
- **Professional Qt Interface**: Cross-platform GUI built with PySide6 for native look and feel
-- **Embedded Matplotlib Integration**: Interactive plots with zoom, pan, and navigation toolbar
-- **Background Processing**: Threading for PCAP loading with progress bar and non-blocking UI
-- **Flow List Widget**: Sortable table with sigma deviations, protocols, and frame types
-- **Signal Visualization**: Click-to-visualize Chapter 10 flows with embedded matplotlib widgets
-- **Threading Safety**: Proper Qt integration prevents matplotlib segmentation faults
+- **Embedded Matplotlib Integration**: Signal plots rendered directly in table cells using FigureCanvas
+- **Main-Thread Plot Creation**: All matplotlib widgets created in GUI main thread for Qt safety
+- **Background PCAP Processing**: Threading only for data loading with progress bar and non-blocking UI
+- **Flow List Widget**: Sortable table with sigma deviations, protocols, and embedded signal plots
+- **Synchronous Plot Rendering**: Plots appear immediately when table populates, no background threads
+- **Threading Safety**: Eliminated Qt threading violations by removing background plot generation
+- **No Floating Windows**: All plots stay embedded in the table interface
```python
class StreamLensMainWindow(QMainWindow):
def __init__(self):
- # Create main interface with flow list and plot area
- self.flows_table = QTableWidget() # Sortable flow list
- self.plot_widget = PlotWidget() # Embedded matplotlib
+ # Create main interface with docking panels
+ self.flow_list_dock = FlowListDockWidget(self) # Flow table with embedded plots
+ self.flow_detail_dock = FlowDetailDockWidget(self) # Detail panel
- def load_pcap_file(self, file_path: str):
- # Background loading with progress bar
- self.loading_thread = PCAPLoadThread(file_path)
- self.loading_thread.progress_updated.connect(self.progress_bar.setValue)
- self.loading_thread.loading_finished.connect(self.on_pcap_loaded)
+ def on_pcap_loaded(self, analyzer):
+ # Populate flows table with embedded plots (synchronous)
+ self.flow_list_dock.populate_flows_table() # Creates plots in main thread
- def visualize_selected_flow(self):
- # Interactive signal visualization
- signal_data = signal_visualizer._extract_signals_from_flow(packets, tmats)
- self.plot_widget.plot_flow_signals(flow, signal_data, flow_key)
+class FlowListDockWidget(QWidget):
+ def _create_integrated_plot_widget(self, flow, flow_key):
+ # Create embedded matplotlib widget in main thread
+ figure = Figure(figsize=(6, 2))
+ canvas = FigureCanvas(figure) # Qt widget created in main thread
+ self._populate_integrated_plot(figure, canvas, flow, flow_key)
+ return plot_widget
```
-### 8. Modular Architecture Design
+### 8. Embedded Plot Architecture (Recent Enhancement)
+- **Qt Threading Compliance**: All matplotlib widgets created in main GUI thread
+- **Synchronous Plot Rendering**: Plots appear immediately when table loads, no async threads
+- **FigureCanvas Integration**: Matplotlib FigureCanvas widgets embedded directly in table cells
+- **No Floating Windows**: Complete elimination of popup matplotlib windows
+- **Signal Caching**: Processed signal data cached to avoid repeated extraction
+- **Main Thread Safety**: Removed PlotGenerationThread to prevent Qt threading violations
+
+```python
+def _create_integrated_plot_widget(self, flow: 'FlowStats', flow_key: str) -> QWidget:
+ """Create matplotlib widget embedded in table cell"""
+ plot_widget = QWidget()
+ layout = QVBoxLayout()
+
+ # Create figure and canvas in main thread
+ figure = Figure(figsize=(6, 2))
+ canvas = FigureCanvas(figure) # Qt widget - must be in main thread
+ canvas.setMaximumHeight(120)
+
+ layout.addWidget(canvas)
+ plot_widget.setLayout(layout)
+
+ # Populate plot synchronously
+ self._populate_integrated_plot(figure, canvas, flow, flow_key)
+ return plot_widget
+```
+
+### 9. Modular Architecture Design
- **Separation of Concerns**: Clean boundaries between analysis, UI, protocols, and utilities
- **Package Structure**: Logical grouping of related functionality
- **Dependency Injection**: Components receive dependencies through constructors
@@ -435,4 +467,89 @@ The project includes comprehensive test suites:
- **TUI Layout Tests**: Interface rendering validation
- **Integration Tests**: End-to-end workflow verification
-This comprehensive description captures the full scope and technical depth of the Ethernet Traffic Analyzer, enabling recreation of this sophisticated telemetry analysis tool.
\ No newline at end of file
+## Current Implementation Status (2025-01-26 - Latest Update)
+
+### ✅ Fully Implemented Features
+- **Core Analysis Engine**: Complete with flow tracking, statistical analysis, and outlier detection
+- **TUI Interface**: Three-panel layout with navigation, timeline visualization, and protocol dissection
+- **GUI Interface**: Professional PySide6 interface with docking panels and embedded plots
+- **Protocol Dissectors**: Chapter 10, PTP, IENA, and standard protocol support
+- **Signal Visualization**: Both TUI (file output) and GUI (embedded plots) working
+- **PCAP Loading**: Background threading with progress bars
+- **Live Capture**: Real-time analysis with network interface monitoring
+- **Statistical Reporting**: Comprehensive outlier analysis and sigma-based flow prioritization
+- **Threading Safety**: Proper Qt integration eliminates segmentation faults
+
+### 🔧 Recent Improvements (Latest Session)
+**Phase 1: Embedded Plot Foundation**
+- **Embedded Plot Integration**: Removed floating windows, plots now embedded in flow table
+- **Qt Threading Compliance**: Fixed threading violations by moving plot creation to main thread
+- **Synchronous Rendering**: Plots appear immediately when table loads
+- **Matplotlib Configuration**: Proper backend setup prevents unwanted popup windows
+- **Signal Visualizer Protection**: GUI mode blocks floating window creation
+
+**Phase 2: Grid Refactoring**
+- **Expanded Data Columns**: Increased from 6 to 10 columns with comprehensive flow data
+- **User-Resizable Columns**: All columns adjustable via QHeaderView.Interactive
+- **Dense Data Display**: Added Max σ, Avg ΔT, Std ΔT, Outliers, and Protocols columns
+
+**Phase 3: Modern Dark Theme Implementation**
+- **Professional Color Palette**: Modern #1e1e1e backgrounds with optimized contrast ratios
+- **Comprehensive Dark Styling**: Applied to tables, headers, scrollbars, menus, and toolbars
+- **Plot Theme Integration**: Matplotlib plots styled with dark backgrounds and modern #0078d4 colors
+- **Color-Coded Data**: Sigma values and outliers highlighted with modern red/amber colors
+
+**Phase 4: Layout Optimization (Final)**
+- **Full-Width Utilization**: Grid view now uses entire screen width effectively
+- **Prioritized Wide Plots**: Increased from 6x2 to 8x2.5 figure size with 600px minimum column width
+- **25% Taller Rows**: Increased from 24px to 30px for better visual balance
+- **Content-Fitted Headers**: Columns auto-resize to fit content, not unnecessarily wide
+- **Optimized Plot Margins**: Reduced horizontal margins from 15%/95% to 8%/98% for maximum signal area
+
+### 🚀 Recommended Usage
+```bash
+# Primary recommended workflow
+python streamlens.py --gui --pcap file.pcap
+
+# For terminal-based analysis
+python streamlens.py --pcap file.pcap
+
+# For comprehensive reporting
+python streamlens.py --pcap file.pcap --report
+```
+
+### 📁 Project Structure Status
+All major components implemented and working:
+- ✅ `analyzer/analysis/` - Core analysis engine
+- ✅ `analyzer/models/` - Data structures
+- ✅ `analyzer/protocols/` - Protocol dissectors
+- ✅ `analyzer/gui/` - Modern GUI with embedded plots
+- ✅ `analyzer/tui/` - Text-based interface
+- ✅ `analyzer/utils/` - Utilities and signal visualization
+
+### 🎯 Key Technical Achievements
+1. **Sigma-Based Flow Prioritization**: Automatically sorts flows by timing outliers
+2. **Optimized Embedded Matplotlib Integration**: Wide plots (8x2.5) with minimal margins in Qt table cells
+3. **Modern Dark Theme UI**: Professional #1e1e1e color scheme with optimal contrast and modern styling
+4. **Content-Adaptive Layout**: Intelligent column sizing with full-width utilization and 25% taller rows
+5. **Multi-Protocol Support**: Specialized dissectors for aviation/industrial protocols
+6. **Real-Time Analysis**: Live capture with running statistics
+7. **Threading-Safe Architecture**: Qt-compliant plot creation eliminates segmentation faults
+8. **Cross-Platform Professional GUI**: PySide6-based interface with native look and feel
+
+### 📊 Current GUI Specifications
+- **Table Layout**: 10 columns with content-fitted headers and user resizing
+- **Row Height**: 30px (25% increase) for improved visual balance
+- **Plot Dimensions**: 8x2.5 matplotlib figures with 150px height and minimal margins
+- **Color Scheme**: Modern dark theme (#1e1e1e) with #0078d4 accents and proper contrast
+- **Width Utilization**: Full screen width with plot column priority (600px minimum + stretching)
+
+### 🏆 Final Implementation Quality
+The StreamLens GUI now represents a professional-grade network analysis interface with:
+- **Pixel-perfect dark theme styling** across all components
+- **Optimized screen real estate usage** with intelligent column sizing
+- **Wide, detailed signal visualizations** embedded directly in the flow table
+- **Threading-safe architecture** preventing crashes and UI blocking
+- **Modern user experience** with instant plot rendering and responsive interactions
+
+This comprehensive description captures the full scope and technical depth of the StreamLens Ethernet Traffic Analyzer, enabling recreation of this sophisticated telemetry analysis tool with its current state-of-the-art GUI implementation.
\ No newline at end of file
diff --git a/analyzer/gui/dock_panels.py b/analyzer/gui/dock_panels.py
new file mode 100644
index 0000000..9d0c8a8
--- /dev/null
+++ b/analyzer/gui/dock_panels.py
@@ -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"File: {os.path.basename(file_path)}
"
+ file_info += f"Packets: {summary['total_packets']:,}
"
+ file_info += f"Flows: {summary['unique_flows']}
"
+ file_info += f"IPs: {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('Select a flow to view detailed information')
+
+ 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('No flow selected')
+ return
+
+ max_sigma = analyzer.statistics_engine.get_max_sigma_deviation(flow)
+
+ html = f"""
+
| Packets: | {flow.frame_count:,} |
| Total Bytes: | {flow.total_bytes:,} |
| Max Sigma Deviation: | {max_sigma:.2f}σ |
| Protocols: | {', '.join(flow.protocols)} |
| Enhanced Protocols: | {", ".join(flow.detected_protocol_types)} |
| Avg Inter-arrival: | {flow.avg_inter_arrival:.6f}s |
| Std Deviation: | {flow.std_inter_arrival:.6f}s |
| Outlier Frames: | {len(flow.outlier_frames)} |
Advanced Ethernet Traffic Analyzer
+Specialized protocol dissection for aviation and industrial networks + with sigma-based outlier identification and interactive signal visualization.
+Features:
+Built with PySide6 and matplotlib
""" + ) + + + 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}σ") \ No newline at end of file diff --git a/analyzer/utils/signal_visualizer.py b/analyzer/utils/signal_visualizer.py index d486d0e..4b05270 100644 --- a/analyzer/utils/signal_visualizer.py +++ b/analyzer/utils/signal_visualizer.py @@ -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}")