progress?

This commit is contained in:
2025-07-28 18:28:26 -04:00
parent 2ab3f1fe9e
commit 8d883f25c3
16 changed files with 2004 additions and 72 deletions

View File

@@ -15,12 +15,15 @@ from rich.console import Group
from rich.panel import Panel
from rich.table import Table
import time
import signal
import sys
from .widgets.sparkline import SparklineWidget
from .widgets.metric_card import MetricCard
from .widgets.flow_table_v2 import EnhancedFlowTable
from .widgets.split_flow_details import FlowMainDetailsPanel, SubFlowDetailsPanel
from .widgets.debug_panel import DebugPanel
from ...analysis.background_analyzer import BackgroundAnalyzer
if TYPE_CHECKING:
from ...analysis.core import EthernetAnalyzer
@@ -50,6 +53,7 @@ class StreamLensAppV2(App):
("4", "sort('quality')", "Sort Quality"),
("p", "toggle_pause", "Pause"),
("d", "show_details", "Details"),
("v", "toggle_view_mode", "Toggle View"),
("?", "toggle_help", "Help"),
]
@@ -73,6 +77,17 @@ class StreamLensAppV2(App):
self.sub_title = "Network Flow Analysis"
self.paused = False
# Background parsing support
self.background_analyzer = BackgroundAnalyzer(
analyzer=analyzer,
num_threads=4,
batch_size=1000,
progress_callback=None,
flow_update_callback=self._on_flow_update
)
self.pcap_file = None
# Metrics history for sparklines
self.packets_history = []
self.bytes_history = []
@@ -127,11 +142,20 @@ class StreamLensAppV2(App):
except:
pass # Debug panel not visible
# Set initial subtitle with view mode
try:
flow_table = self.query_one("#flow-table", EnhancedFlowTable)
view_mode = flow_table.get_current_view_mode()
status = "PAUSED" if self.paused else "LIVE"
self.sub_title = f"Network Flow Analysis - {status} - {view_mode} VIEW"
except:
pass
self.update_metrics()
# Set up update intervals like TipTop
self.metric_timer = self.set_interval(0.5, self.update_metrics) # 2Hz for smooth graphs
self.flow_timer = self.set_interval(1.0, self.update_flows) # 1Hz for flow data
# Set up update intervals like TipTop (reduced frequency since we have real-time updates)
self.metric_timer = self.set_interval(2.0, self.update_metrics) # 0.5Hz for background updates
self.flow_timer = self.set_interval(5.0, self.update_flows) # 0.2Hz for fallback flow updates
# Initialize sparkline history
self._initialize_history()
@@ -177,13 +201,23 @@ class StreamLensAppV2(App):
self.packets_per_sec = self.total_packets / elapsed
self.bytes_per_sec = summary.get('total_bytes', 0) / elapsed
# Count enhanced and outliers
# Count enhanced and outliers (thread-safe access)
enhanced = 0
outliers = 0
for flow in self.analyzer.flows.values():
if flow.enhanced_analysis.decoder_type != "Standard":
enhanced += 1
outliers += len(flow.outlier_frames)
try:
# Use background analyzer's thread-safe flow access
flows = self.background_analyzer.get_current_flows()
for flow in flows.values():
if flow.enhanced_analysis.decoder_type != "Standard":
enhanced += 1
outliers += len(flow.outlier_frames)
except Exception:
# Fallback to direct access if background analyzer not available
for flow in self.analyzer.flows.values():
if flow.enhanced_analysis.decoder_type != "Standard":
enhanced += 1
outliers += len(flow.outlier_frames)
self.enhanced_flows = enhanced
self.outlier_count = outliers
@@ -256,6 +290,53 @@ class StreamLensAppV2(App):
flow_table = self.query_one("#flow-table", EnhancedFlowTable)
flow_table.refresh_data()
def _on_flow_update(self):
"""Handle flow data updates from background parser"""
try:
# Use call_from_thread to safely update UI from background thread
self.call_from_thread(self._update_flow_ui)
except Exception:
# Ignore errors during shutdown
pass
def _update_flow_ui(self):
"""Update flow UI (called from main thread)"""
try:
# Update flow table
flow_table = self.query_one("#flow-table", EnhancedFlowTable)
flow_table.refresh_data()
# Also update metrics in real-time
self.update_metrics()
except Exception:
# Flow table widget may not be available yet
pass
def start_background_parsing(self, pcap_file: str):
"""Start parsing PCAP file in background"""
self.pcap_file = pcap_file
# Start background parsing
self.background_analyzer.start_parsing(pcap_file)
def stop_background_parsing(self):
"""Stop background parsing"""
self.background_analyzer.stop_parsing()
def cleanup(self):
"""Cleanup resources when app shuts down"""
try:
self.background_analyzer.cleanup()
# Cancel any pending timers
if self.metric_timer:
self.metric_timer.stop()
if self.flow_timer:
self.flow_timer.stop()
except Exception as e:
# Don't let cleanup errors prevent shutdown
pass
def on_enhanced_flow_table_flow_selected(self, event: EnhancedFlowTable.FlowSelected) -> None:
"""Handle flow selection events"""
try:
@@ -290,7 +371,14 @@ class StreamLensAppV2(App):
"""Toggle pause state"""
self.paused = not self.paused
status = "PAUSED" if self.paused else "LIVE"
self.sub_title = f"Network Flow Analysis - {status}"
# Get current view mode to maintain it in subtitle
try:
flow_table = self.query_one("#flow-table", EnhancedFlowTable)
view_mode = flow_table.get_current_view_mode()
self.sub_title = f"Network Flow Analysis - {status} - {view_mode} VIEW"
except:
self.sub_title = f"Network Flow Analysis - {status}"
def action_sort(self, key: str) -> None:
"""Sort flow table by specified key"""
@@ -302,10 +390,29 @@ class StreamLensAppV2(App):
# TODO: Implement detailed flow modal
pass
def action_toggle_view_mode(self) -> None:
"""Toggle between simplified and detailed view modes"""
flow_table = self.query_one("#flow-table", EnhancedFlowTable)
flow_table.toggle_view_mode()
# Update subtitle to show current view mode
view_mode = flow_table.get_current_view_mode()
status = "PAUSED" if self.paused else "LIVE"
self.sub_title = f"Network Flow Analysis - {status} - {view_mode} VIEW"
def on_mouse_down(self, event: MouseDown) -> None:
"""Prevent default mouse down behavior to disable mouse interaction."""
event.prevent_default()
def on_mouse_move(self, event: MouseMove) -> None:
"""Prevent default mouse move behavior to disable mouse interaction."""
event.prevent_default()
event.prevent_default()
def action_quit(self) -> None:
"""Quit the application with proper cleanup"""
self.cleanup()
self.exit()
def on_unmount(self) -> None:
"""Called when app is being unmounted - ensure cleanup"""
self.cleanup()

View File

@@ -6,7 +6,7 @@ from textual.widgets import DataTable
from textual.containers import Vertical
from textual.reactive import reactive
from textual.message import Message
from typing import TYPE_CHECKING, List, Optional
from typing import TYPE_CHECKING, List, Optional, Dict, Tuple
from rich.text import Text
from rich.box import ROUNDED
@@ -43,6 +43,7 @@ class EnhancedFlowTable(Vertical):
selected_flow_index = reactive(0)
sort_key = reactive("flows")
simplified_view = reactive(False) # Toggle between detailed and simplified view
def __init__(self, analyzer: 'EthernetAnalyzer', **kwargs):
super().__init__(**kwargs)
@@ -50,6 +51,7 @@ class EnhancedFlowTable(Vertical):
self.flows_list = []
self.row_to_flow_map = {} # Map row keys to flow indices
self.flow_metrics = {} # Store per-flow metrics history
self.view_mode_changed = False # Track when view mode changes
def compose(self):
"""Create the enhanced flow table"""
@@ -64,25 +66,49 @@ class EnhancedFlowTable(Vertical):
def on_mount(self):
"""Initialize the table"""
table = self.query_one("#flows-data-table", DataTable)
# Compact columns optimized for data density
table.add_column("#", width=2, key="num")
table.add_column("Source", width=18, key="source")
table.add_column("Proto", width=4, key="proto")
table.add_column("Destination", width=18, key="dest")
table.add_column("Extended", width=8, key="extended")
table.add_column("Frame Type", width=10, key="frame_type")
table.add_column("Pkts", width=6, key="rate")
table.add_column("Size", width=8, key="volume")
table.add_column("ΔT(ms)", width=8, key="delta_t")
table.add_column("σ(ms)", width=8, key="sigma")
table.add_column("Out", width=5, key="outliers")
self._setup_table_columns()
self.refresh_data()
def _setup_table_columns(self):
"""Setup table columns based on current view mode"""
table = self.query_one("#flows-data-table", DataTable)
# Clear existing columns if any
if table.columns:
table.clear(columns=True)
if self.simplified_view:
# Simplified view - only main flows with summary data
table.add_column("#", width=3, key="num")
table.add_column("Source", width=18, key="source")
table.add_column("Destination", width=18, key="dest")
table.add_column("Protocol", width=8, key="protocol")
table.add_column("Packets", width=8, key="packets")
table.add_column("Volume", width=10, key="volume")
table.add_column("Avg ΔT", width=8, key="avg_delta")
table.add_column("Quality", width=8, key="quality")
table.add_column("Status", width=10, key="status")
else:
# Detailed view - original layout with subflows
table.add_column("#", width=2, key="num")
table.add_column("Source", width=18, key="source")
table.add_column("Proto", width=4, key="proto")
table.add_column("Destination", width=18, key="dest")
table.add_column("Extended", width=8, key="extended")
table.add_column("Frame Type", width=10, key="frame_type")
table.add_column("Pkts", width=6, key="rate")
table.add_column("Size", width=8, key="volume")
table.add_column("ΔT(ms)", width=8, key="delta_t")
table.add_column("σ(ms)", width=8, key="sigma")
table.add_column("Out", width=5, key="outliers")
def refresh_data(self):
"""Refresh flow table with enhanced visualizations"""
"""Refresh flow table with current view mode"""
# Check if view mode changed and rebuild table structure if needed
if self.view_mode_changed:
self._setup_table_columns()
self.view_mode_changed = False
table = self.query_one("#flows-data-table", DataTable)
# Preserve cursor and scroll positions
@@ -103,7 +129,39 @@ class EnhancedFlowTable(Vertical):
# Get and sort flows
self.flows_list = self._get_sorted_flows()
# Add flows with enhanced display
if self.simplified_view:
self._populate_simplified_view()
else:
self._populate_detailed_view()
# Restore cursor position
if selected_row_key and selected_row_key in table.rows:
row_index = list(table.rows.keys()).index(selected_row_key)
table.move_cursor(row=row_index, column=cursor_column, animate=False)
elif table.row_count > 0:
# If original selection not found, try to maintain row position
new_row = min(cursor_row, table.row_count - 1)
table.move_cursor(row=new_row, column=cursor_column, animate=False)
# Restore scroll position
table.scroll_to(x=scroll_x, y=scroll_y, animate=False)
def _populate_simplified_view(self):
"""Populate table with simplified flow summary data"""
table = self.query_one("#flows-data-table", DataTable)
for i, flow in enumerate(self.flows_list):
# Create simplified row data - no subflows shown
row_data = self._create_simplified_row(i + 1, flow)
row_key = table.add_row(*row_data, key=f"flow_{i}")
# Map row key to flow index
self.row_to_flow_map[row_key] = i
def _populate_detailed_view(self):
"""Populate table with detailed flow data including subflows"""
table = self.query_one("#flows-data-table", DataTable)
for i, flow in enumerate(self.flows_list):
# Track metrics for this flow
flow_key = f"{flow.src_ip}:{flow.src_port}-{flow.dst_ip}:{flow.dst_port}"
@@ -127,20 +185,14 @@ class EnhancedFlowTable(Vertical):
metrics['last_packet_count'] = flow.frame_count
metrics['last_update'] = flow.last_seen
# Create row with visualizations
# Create row with detailed visualizations
row_data = self._create_enhanced_row(i + 1, flow, metrics)
row_key = table.add_row(*row_data, key=f"flow_{i}")
# Map row key to flow index
self.row_to_flow_map[row_key] = i
# Apply row styling based on status
style = self._get_flow_style(flow)
if style:
# Note: DataTable doesn't have set_row_style, using CSS classes instead
pass
# Add sub-rows for protocol breakdown
# Add sub-rows for protocol breakdown (only in detailed view)
if self._should_show_subrows(flow):
sub_rows = self._create_protocol_subrows(flow)
combinations = self._get_protocol_frame_combinations(flow)
@@ -151,18 +203,6 @@ class EnhancedFlowTable(Vertical):
if j < len(combinations):
_, frame_type, _, _ = combinations[j]
self.row_to_subflow_map[sub_key] = (i, frame_type)
# Restore cursor position
if selected_row_key and selected_row_key in table.rows:
row_index = list(table.rows.keys()).index(selected_row_key)
table.move_cursor(row=row_index, column=cursor_column, animate=False)
elif table.row_count > 0:
# If original selection not found, try to maintain row position
new_row = min(cursor_row, table.row_count - 1)
table.move_cursor(row=new_row, column=cursor_column, animate=False)
# Restore scroll position
table.scroll_to(x=scroll_x, y=scroll_y, animate=False)
def _create_enhanced_row(self, num: int, flow: 'FlowStats', metrics: dict) -> List[Text]:
"""Create enhanced row with inline visualizations"""
@@ -229,6 +269,64 @@ class EnhancedFlowTable(Vertical):
delta_t_text, sigma_text, outlier_text
]
def _create_simplified_row(self, num: int, flow: 'FlowStats') -> List[Text]:
"""Create simplified row with summary data only"""
# Flow number
num_text = Text(str(num), justify="right")
# Source (IP only for simplified view)
source_text = Text(flow.src_ip)
# Destination (IP only for simplified view)
dest_text = Text(flow.dst_ip)
# Main protocol (transport + extended if available)
extended = self._get_extended_protocol(flow)
if extended != "-":
protocol_str = f"{flow.transport_protocol}/{extended}"
else:
protocol_str = flow.transport_protocol
protocol_text = Text(protocol_str, style="bold cyan")
# Total packet count
packets_text = Text(str(flow.frame_count), justify="right")
# Total volume
volume_text = Text(self._format_bytes(flow.total_bytes), justify="right")
# Average delta T
if flow.avg_inter_arrival > 0:
delta_t_ms = flow.avg_inter_arrival * 1000
if delta_t_ms >= 1000:
avg_delta_str = f"{delta_t_ms/1000:.1f}s"
else:
avg_delta_str = f"{delta_t_ms:.1f}ms"
else:
avg_delta_str = "N/A"
avg_delta_text = Text(avg_delta_str, justify="right")
# Quality score as percentage
quality_score = self._get_quality_score(flow)
quality_text = Text(f"{quality_score}%", justify="right",
style="green" if quality_score >= 90 else
"yellow" if quality_score >= 70 else "red")
# Flow status
status = self._get_flow_status(flow)
status_color = {
"Enhanced": "bold blue",
"Alert": "bold red",
"Warning": "yellow",
"Normal": "green"
}.get(status, "white")
status_text = Text(status, style=status_color)
return [
num_text, source_text, dest_text, protocol_text,
packets_text, volume_text, avg_delta_text,
quality_text, status_text
]
def _create_rate_sparkline(self, history: List[float]) -> str:
"""Create mini sparkline for rate"""
if not history:
@@ -319,16 +417,60 @@ class EnhancedFlowTable(Vertical):
def _should_show_subrows(self, flow: 'FlowStats') -> bool:
"""Determine if flow should show protocol breakdown"""
# Show subrows for flows with multiple frame types or enhanced analysis
return (len(flow.frame_types) > 1 or
flow.enhanced_analysis.decoder_type != "Standard")
# Only show subrows if there are enhanced frame types
enhanced_frame_types = self._get_enhanced_frame_types(flow)
return len(enhanced_frame_types) > 0
def _get_enhanced_frame_types(self, flow: 'FlowStats') -> Dict[str, 'FrameTypeStats']:
"""Get only frame types that belong to enhanced protocols"""
enhanced_protocols = {'CHAPTER10', 'CH10', 'PTP', 'IENA'}
enhanced_frame_types = {}
for frame_type, stats in flow.frame_types.items():
# Check if this frame type belongs to an enhanced protocol
if any(enhanced_proto in frame_type for enhanced_proto in enhanced_protocols):
enhanced_frame_types[frame_type] = stats
elif frame_type.startswith(('CH10-', 'PTP-', 'IENA-')):
enhanced_frame_types[frame_type] = stats
elif frame_type in ('TMATS', 'TMATS-Data'): # TMATS is part of Chapter 10
enhanced_frame_types[frame_type] = stats
return enhanced_frame_types
def _get_enhanced_protocol_frame_combinations(self, flow: 'FlowStats', enhanced_frame_types: Dict[str, 'FrameTypeStats']) -> List[Tuple[str, str, int, float]]:
"""Get protocol/frame combinations for enhanced protocols only"""
combinations = []
total_packets = flow.frame_count
# Group enhanced frame types by extended protocol
protocol_frames = {}
for frame_type, ft_stats in enhanced_frame_types.items():
# Determine extended protocol for this frame type
extended_proto = self._get_extended_protocol_for_frame(flow, frame_type)
if extended_proto not in protocol_frames:
protocol_frames[extended_proto] = []
protocol_frames[extended_proto].append((frame_type, ft_stats.count))
# Convert to list of tuples with percentages
for extended_proto, frame_list in protocol_frames.items():
for frame_type, count in frame_list:
percentage = (count / total_packets * 100) if total_packets > 0 else 0
combinations.append((extended_proto, frame_type, count, percentage))
# Sort by count (descending)
combinations.sort(key=lambda x: x[2], reverse=True)
return combinations
def _create_protocol_subrows(self, flow: 'FlowStats') -> List[List[Text]]:
"""Create sub-rows for protocol/frame type breakdown"""
"""Create sub-rows for enhanced protocol/frame type breakdown only"""
subrows = []
combinations = self._get_protocol_frame_combinations(flow)
enhanced_frame_types = self._get_enhanced_frame_types(flow)
combinations = self._get_enhanced_protocol_frame_combinations(flow, enhanced_frame_types)
for extended_proto, frame_type, count, percentage in combinations: # Show all subrows
for extended_proto, frame_type, count, percentage in combinations: # Show all enhanced subrows
# Calculate timing for this frame type if available
frame_delta_t = ""
frame_sigma = ""
@@ -385,6 +527,16 @@ class EnhancedFlowTable(Vertical):
self.sort_key = key
self.refresh_data()
def toggle_view_mode(self):
"""Toggle between simplified and detailed view modes"""
self.simplified_view = not self.simplified_view
self.view_mode_changed = True
self.refresh_data()
def get_current_view_mode(self) -> str:
"""Get current view mode as string"""
return "SIMPLIFIED" if self.simplified_view else "DETAILED"
class FlowSelected(Message):
"""Message sent when a flow is selected"""
def __init__(self, flow: Optional['FlowStats'], subflow_type: Optional[str] = None) -> None:
@@ -451,6 +603,19 @@ class EnhancedFlowTable(Vertical):
self.post_message(self.FlowSelected(selected_flow, subflow_type))
# Helper methods from original implementation
def _get_extended_protocol_for_frame(self, flow: 'FlowStats', frame_type: str) -> str:
"""Get extended protocol for a specific frame type"""
if frame_type.startswith('CH10') or frame_type == 'TMATS':
return 'CH10'
elif frame_type.startswith('PTP'):
return 'PTP'
elif frame_type == 'IENA':
return 'IENA'
elif frame_type == 'NTP':
return 'NTP'
else:
return self._get_extended_protocol(flow)
def _get_extended_protocol(self, flow: 'FlowStats') -> str:
"""Get extended protocol"""
if flow.detected_protocol_types:

View File

@@ -0,0 +1,121 @@
"""
Progress Bar Widget for PCAP parsing progress
"""
from textual.widget import Widget
from textual.reactive import reactive
from rich.console import RenderableType
from rich.progress import Progress, BarColumn, TextColumn, TaskProgressColumn, MofNCompleteColumn, TimeRemainingColumn
from rich.text import Text
import time
class ParsingProgressBar(Widget):
"""Progress bar widget for PCAP parsing with rich formatting"""
DEFAULT_CSS = """
ParsingProgressBar {
height: 3;
margin: 1;
padding: 1;
background: $surface;
border: solid $accent;
}
"""
# Reactive attributes
progress = reactive(0.0)
total_packets = reactive(0)
processed_packets = reactive(0)
packets_per_second = reactive(0.0)
estimated_time_remaining = reactive(0.0)
is_complete = reactive(False)
is_visible = reactive(False)
error_message = reactive("")
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.start_time = None
def render(self) -> RenderableType:
"""Render the progress bar"""
if not self.is_visible:
return Text("")
if self.error_message:
return Text(f"❌ Error: {self.error_message}", style="red")
if self.is_complete:
elapsed = time.time() - self.start_time if self.start_time else 0
return Text(
f"✅ Parsing complete! {self.processed_packets:,} packets processed in {elapsed:.1f}s",
style="green"
)
# Create rich progress bar
progress = Progress(
TextColumn("[bold blue]Parsing PCAP..."),
BarColumn(bar_width=40),
TaskProgressColumn(),
MofNCompleteColumn(),
TextColumn(""),
TextColumn("{task.fields[rate]}"),
TextColumn(""),
TimeRemainingColumn(),
expand=False
)
# Format rate display
if self.packets_per_second >= 1000:
rate_str = f"{self.packets_per_second/1000:.1f}K pkt/s"
else:
rate_str = f"{self.packets_per_second:.0f} pkt/s"
task = progress.add_task(
"parsing",
total=self.total_packets,
completed=self.processed_packets,
rate=rate_str
)
return progress
def start_parsing(self, total_packets: int):
"""Start showing progress for parsing"""
self.total_packets = total_packets
self.processed_packets = 0
self.progress = 0.0
self.is_complete = False
self.is_visible = True
self.error_message = ""
self.start_time = time.time()
self.refresh()
def update_progress(self, processed: int, total: int, pps: float, eta: float):
"""Update progress values"""
self.processed_packets = processed
self.total_packets = total
self.packets_per_second = pps
self.estimated_time_remaining = eta
self.progress = (processed / total * 100) if total > 0 else 0
self.refresh()
def complete_parsing(self):
"""Mark parsing as complete"""
self.is_complete = True
self.refresh()
# Hide after 3 seconds
self.set_timer(3.0, self.hide_progress)
def show_error(self, error: str):
"""Show error message"""
self.error_message = error
self.is_visible = True
self.refresh()
# Hide after 5 seconds
self.set_timer(5.0, self.hide_progress)
def hide_progress(self):
"""Hide the progress bar"""
self.is_visible = False
self.refresh()

View File

@@ -10,7 +10,7 @@ from rich.text import Text
from rich.panel import Panel
from rich.console import RenderableType, Group
from rich.table import Table
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Optional, Dict
if TYPE_CHECKING:
from ....models import FlowStats, FrameTypeStats
@@ -106,19 +106,18 @@ class FlowMainDetailsPanel(Vertical):
sections.append(Text("Enhanced Analysis", style="bold green"))
sections.append(enhanced_table)
# Timing analysis - only show if no sub-flows exist
# Timing analysis - only show if no enhanced sub-flows exist
# Match the same logic as _should_show_subrows in flow_table_v2.py
has_subflows = (len(flow.frame_types) > 1 or
flow.enhanced_analysis.decoder_type != "Standard")
has_enhanced_subflows = self._has_enhanced_subflows(flow)
# Debug output
try:
debug_panel = self.app.query_one("#debug-panel")
debug_panel.add_debug_message(f"TIMING_LOGIC: {flow.src_ip}:{flow.src_port} - types={len(flow.frame_types)}, decoder={flow.enhanced_analysis.decoder_type}, has_subflows={has_subflows}")
debug_panel.add_debug_message(f"TIMING_LOGIC: {flow.src_ip}:{flow.src_port} - types={len(flow.frame_types)}, decoder={flow.enhanced_analysis.decoder_type}, has_enhanced_subflows={has_enhanced_subflows}")
except:
pass
if not has_subflows:
if not has_enhanced_subflows:
try:
debug_panel = self.app.query_one("#debug-panel")
debug_panel.add_debug_message(f"BRANCH: Taking FULL timing branch for {flow.src_ip}:{flow.src_port}")
@@ -160,6 +159,19 @@ class FlowMainDetailsPanel(Vertical):
return Group(*sections)
def _has_enhanced_subflows(self, flow: 'FlowStats') -> bool:
"""Check if flow has enhanced frame types that warrant sub-rows"""
enhanced_protocols = {'CHAPTER10', 'CH10', 'PTP', 'IENA'}
for frame_type in flow.frame_types.keys():
# Check if this frame type belongs to an enhanced protocol
if any(enhanced_proto in frame_type for enhanced_proto in enhanced_protocols):
return True
elif frame_type.startswith(('CH10-', 'PTP-', 'IENA-')):
return True
return False
def _format_bytes(self, bytes_count: int) -> str:
"""Format byte count with units"""
if bytes_count >= 1_000_000_000:
@@ -276,14 +288,23 @@ class SubFlowDetailsPanel(Vertical):
return Group(*sections)
def _create_subflow_summary(self, flow: 'FlowStats') -> RenderableType:
"""Create summary of all sub-flows"""
if not flow.frame_types or len(flow.frame_types) <= 1:
"""Create summary of all sub-flows for enhanced flows"""
# For enhanced flows, show ALL frame types, not just enhanced ones
if flow.enhanced_analysis.decoder_type != "Standard":
frame_types_to_show = flow.frame_types
title = "Sub-Flow Summary (All Frame Types)"
else:
# For standard flows, only show enhanced frame types if any
frame_types_to_show = self._get_enhanced_frame_types(flow)
title = "Enhanced Sub-Flow Summary"
if not frame_types_to_show:
return Text("No sub-flows available", style="dim")
sections = []
sections.append(Text("Sub-Flow Summary", style="bold yellow"))
sections.append(Text(title, style="bold yellow"))
# Frame type breakdown table
# Frame type breakdown table for enhanced protocols only
frame_table = Table(show_header=True, box=None)
frame_table.add_column("Frame Type", style="blue")
frame_table.add_column("Count", justify="right")
@@ -294,7 +315,7 @@ class SubFlowDetailsPanel(Vertical):
total = flow.frame_count
for frame_type, stats in sorted(
flow.frame_types.items(),
frame_types_to_show.items(),
key=lambda x: x[1].count,
reverse=True
):
@@ -315,6 +336,22 @@ class SubFlowDetailsPanel(Vertical):
sections.append(frame_table)
return Group(*sections)
def _get_enhanced_frame_types(self, flow: 'FlowStats') -> Dict[str, 'FrameTypeStats']:
"""Get only frame types that belong to enhanced protocols"""
enhanced_protocols = {'CHAPTER10', 'CH10', 'PTP', 'IENA'}
enhanced_frame_types = {}
for frame_type, stats in flow.frame_types.items():
# Check if this frame type belongs to an enhanced protocol
if any(enhanced_proto in frame_type for enhanced_proto in enhanced_protocols):
enhanced_frame_types[frame_type] = stats
elif frame_type.startswith(('CH10-', 'PTP-', 'IENA-')):
enhanced_frame_types[frame_type] = stats
elif frame_type in ('TMATS', 'TMATS-Data'): # TMATS is part of Chapter 10
enhanced_frame_types[frame_type] = stats
return enhanced_frame_types
def _format_bytes(self, bytes_count: int) -> str:
"""Format byte count with units"""
if bytes_count >= 1_000_000_000: