- Re-enable kp.core.set_timeout() which is required for proper device communication - Fix GUI terminal truncation issue by using append() instead of setPlainText() - Remove aggressive line limiting that was causing log display to stop midway - Implement gentler memory management (trim only after 1000+ lines) - This should resolve pipeline timeout issues and complete log display The previous USB timeout disable was causing stage timeouts without inference results. The terminal display issue was due to frequent text replacement causing display corruption. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
996 lines
41 KiB
Python
996 lines
41 KiB
Python
"""
|
|
Pipeline Deployment Dialog
|
|
|
|
This dialog handles the conversion of .mflow pipeline data to executable format
|
|
and deployment to Kneron dongles using the InferencePipeline system.
|
|
|
|
Main Components:
|
|
- Pipeline conversion using MFlowConverter
|
|
- Topology analysis and optimization
|
|
- Dongle status monitoring
|
|
- Real-time deployment progress
|
|
- Error handling and troubleshooting
|
|
|
|
Usage:
|
|
from ui.dialogs.deployment import DeploymentDialog
|
|
|
|
dialog = DeploymentDialog(pipeline_data, parent=self)
|
|
dialog.exec_()
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import threading
|
|
import traceback
|
|
import io
|
|
import contextlib
|
|
from typing import Dict, Any, List, Optional
|
|
from PyQt5.QtWidgets import (
|
|
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QTextEdit, QPushButton,
|
|
QProgressBar, QTabWidget, QWidget, QFormLayout, QLineEdit, QSpinBox,
|
|
QCheckBox, QGroupBox, QScrollArea, QTableWidget, QTableWidgetItem,
|
|
QHeaderView, QMessageBox, QSplitter, QFrame
|
|
)
|
|
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
|
|
from PyQt5.QtGui import QFont, QColor, QPalette, QImage, QPixmap
|
|
|
|
# Import our converter and pipeline system
|
|
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'core', 'functions'))
|
|
|
|
try:
|
|
from ...core.functions.mflow_converter import MFlowConverter, PipelineConfig
|
|
CONVERTER_AVAILABLE = True
|
|
except ImportError as e:
|
|
print(f"Warning: MFlow converter not available: {e}")
|
|
CONVERTER_AVAILABLE = False
|
|
|
|
try:
|
|
from ...core.functions.Multidongle import MultiDongle
|
|
from ...core.functions.InferencePipeline import InferencePipeline
|
|
from ...core.functions.workflow_orchestrator import WorkflowOrchestrator
|
|
# from workflow_orchestrator import WorkflowOrchestrator
|
|
PIPELINE_AVAILABLE = True
|
|
except ImportError as e:
|
|
print(f"Warning: Pipeline system not available: {e}")
|
|
PIPELINE_AVAILABLE = False
|
|
|
|
|
|
class StdoutCapture:
|
|
"""Context manager to capture stdout/stderr and emit to signal."""
|
|
|
|
def __init__(self, signal_emitter):
|
|
self.signal_emitter = signal_emitter
|
|
self.original_stdout = None
|
|
self.original_stderr = None
|
|
self.captured_output = io.StringIO()
|
|
|
|
def __enter__(self):
|
|
self.original_stdout = sys.stdout
|
|
self.original_stderr = sys.stderr
|
|
|
|
# Create a custom write function that both prints to original and captures
|
|
class TeeWriter:
|
|
def __init__(self, original, captured, emitter):
|
|
self.original = original
|
|
self.captured = captured
|
|
self.emitter = emitter
|
|
self._emitting = False # Prevent recursion
|
|
|
|
def write(self, text):
|
|
# Write to original stdout/stderr (so it still appears in terminal)
|
|
self.original.write(text)
|
|
self.original.flush()
|
|
|
|
# Capture for GUI if it's a substantial message and not already emitting
|
|
if text.strip() and not self._emitting:
|
|
self._emitting = True
|
|
try:
|
|
self.emitter(text)
|
|
finally:
|
|
self._emitting = False
|
|
|
|
def flush(self):
|
|
self.original.flush()
|
|
|
|
# Replace stdout and stderr with our tee writers
|
|
sys.stdout = TeeWriter(self.original_stdout, self.captured_output, self.signal_emitter)
|
|
sys.stderr = TeeWriter(self.original_stderr, self.captured_output, self.signal_emitter)
|
|
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
# Restore original stdout/stderr
|
|
sys.stdout = self.original_stdout
|
|
sys.stderr = self.original_stderr
|
|
|
|
|
|
class DeploymentWorker(QThread):
|
|
"""Worker thread for pipeline deployment to avoid blocking UI."""
|
|
|
|
# Signals
|
|
progress_updated = pyqtSignal(int, str) # progress, message
|
|
topology_analyzed = pyqtSignal(dict) # topology analysis results
|
|
conversion_completed = pyqtSignal(object) # PipelineConfig object
|
|
deployment_started = pyqtSignal()
|
|
deployment_completed = pyqtSignal(bool, str) # success, message
|
|
error_occurred = pyqtSignal(str)
|
|
frame_updated = pyqtSignal('PyQt_PyObject') # For live view
|
|
result_updated = pyqtSignal(dict) # For inference results
|
|
terminal_output = pyqtSignal(str) # For terminal output in GUI
|
|
stdout_captured = pyqtSignal(str) # For captured stdout/stderr
|
|
|
|
def __init__(self, pipeline_data: Dict[str, Any]):
|
|
super().__init__()
|
|
self.pipeline_data = pipeline_data
|
|
self.should_stop = False
|
|
self.orchestrator = None
|
|
|
|
def run(self):
|
|
"""Main deployment workflow."""
|
|
try:
|
|
# Step 1: Convert .mflow to pipeline config
|
|
self.progress_updated.emit(10, "Converting pipeline configuration...")
|
|
|
|
if not CONVERTER_AVAILABLE:
|
|
self.error_occurred.emit("MFlow converter not available. Please check installation.")
|
|
return
|
|
|
|
converter = MFlowConverter()
|
|
config = converter._convert_mflow_to_config(self.pipeline_data)
|
|
|
|
# Emit topology analysis results
|
|
self.topology_analyzed.emit({
|
|
'total_stages': len(config.stage_configs),
|
|
'pipeline_name': config.pipeline_name,
|
|
'input_config': config.input_config,
|
|
'output_config': config.output_config
|
|
})
|
|
|
|
self.progress_updated.emit(30, "Pipeline conversion completed")
|
|
self.conversion_completed.emit(config)
|
|
|
|
if self.should_stop:
|
|
return
|
|
|
|
# Step 2: Validate configuration
|
|
self.progress_updated.emit(40, "Validating pipeline configuration...")
|
|
is_valid, errors = converter.validate_config(config)
|
|
|
|
if not is_valid:
|
|
error_msg = "Configuration validation failed:\n" + "\n".join(errors)
|
|
self.error_occurred.emit(error_msg)
|
|
return
|
|
|
|
self.progress_updated.emit(60, "Configuration validation passed")
|
|
|
|
if self.should_stop:
|
|
return
|
|
|
|
# Step 3: Initialize pipeline (if dongle system available)
|
|
self.progress_updated.emit(70, "Initializing inference pipeline...")
|
|
|
|
if not PIPELINE_AVAILABLE:
|
|
self.progress_updated.emit(100, "Pipeline configuration ready (dongle system not available)")
|
|
self.deployment_completed.emit(True, "Pipeline configuration prepared successfully. Dongle system not available for actual deployment.")
|
|
return
|
|
|
|
# Create InferencePipeline instance with stdout capture
|
|
try:
|
|
# Capture all stdout/stderr during pipeline operations
|
|
with StdoutCapture(self.stdout_captured.emit):
|
|
pipeline = converter.create_inference_pipeline(config)
|
|
|
|
self.progress_updated.emit(80, "Initializing workflow orchestrator...")
|
|
self.deployment_started.emit()
|
|
|
|
# Create and start the orchestrator
|
|
self.orchestrator = WorkflowOrchestrator(pipeline, config.input_config, config.output_config)
|
|
self.orchestrator.set_frame_callback(self.frame_updated.emit)
|
|
|
|
# Set up both GUI and terminal result callbacks
|
|
def combined_result_callback(result_dict):
|
|
# Add current FPS from pipeline to result_dict
|
|
result_dict['current_pipeline_fps'] = pipeline.get_current_fps()
|
|
|
|
# Send to GUI terminal and results display
|
|
terminal_output = self._format_terminal_results(result_dict)
|
|
self.terminal_output.emit(terminal_output)
|
|
# Emit for GUI
|
|
self.result_updated.emit(result_dict)
|
|
|
|
self.orchestrator.set_result_callback(combined_result_callback)
|
|
|
|
self.orchestrator.start()
|
|
|
|
self.progress_updated.emit(100, "Pipeline deployed successfully!")
|
|
self.deployment_completed.emit(True, f"Pipeline '{config.pipeline_name}' deployed with {len(config.stage_configs)} stages")
|
|
|
|
# Keep running until stop is requested with continued stdout capture
|
|
while not self.should_stop:
|
|
self.msleep(100) # Sleep for 100ms and check again
|
|
|
|
except Exception as e:
|
|
self.error_occurred.emit(f"Pipeline deployment failed: {str(e)}")
|
|
|
|
except Exception as e:
|
|
self.error_occurred.emit(f"Deployment error: {str(e)}")
|
|
|
|
def stop(self):
|
|
"""Stop the deployment process."""
|
|
self.should_stop = True
|
|
if self.orchestrator:
|
|
self.orchestrator.stop()
|
|
|
|
def _format_terminal_results(self, result_dict):
|
|
"""Format inference results for terminal display in GUI."""
|
|
try:
|
|
from datetime import datetime
|
|
|
|
# Header with timestamp
|
|
timestamp = datetime.fromtimestamp(result_dict.get('timestamp', 0)).strftime("%H:%M:%S.%f")[:-3]
|
|
pipeline_id = result_dict.get('pipeline_id', 'Unknown')
|
|
|
|
output_lines = []
|
|
output_lines.append(f"\n🔥 INFERENCE RESULT [{timestamp}]")
|
|
output_lines.append(f" Pipeline ID: {pipeline_id}")
|
|
output_lines.append(" " + "="*50)
|
|
|
|
# Stage results
|
|
stage_results = result_dict.get('stage_results', {})
|
|
if stage_results:
|
|
for stage_id, result in stage_results.items():
|
|
output_lines.append(f" 📊 Stage: {stage_id}")
|
|
|
|
if isinstance(result, tuple) and len(result) == 2:
|
|
# Handle tuple results (probability, result_string) - matching actual format
|
|
probability, result_string = result
|
|
output_lines.append(f" ✅ Result: {result_string}")
|
|
output_lines.append(f" 📈 Probability: {probability:.3f}")
|
|
|
|
# Add confidence level
|
|
if probability > 0.8:
|
|
confidence = "🟢 Very High"
|
|
elif probability > 0.6:
|
|
confidence = "🟡 High"
|
|
elif probability > 0.4:
|
|
confidence = "🟠 Medium"
|
|
else:
|
|
confidence = "🔴 Low"
|
|
output_lines.append(f" 🎯 Confidence: {confidence}")
|
|
|
|
elif isinstance(result, dict):
|
|
# Handle dict results
|
|
for key, value in result.items():
|
|
if key == 'probability':
|
|
output_lines.append(f" 📈 {key.title()}: {value:.3f}")
|
|
elif key == 'result':
|
|
output_lines.append(f" ✅ {key.title()}: {value}")
|
|
elif key == 'confidence':
|
|
output_lines.append(f" 🎯 {key.title()}: {value}")
|
|
elif key == 'fused_probability':
|
|
output_lines.append(f" 🔀 Fused Probability: {value:.3f}")
|
|
elif key == 'individual_probs':
|
|
output_lines.append(f" 📋 Individual Probabilities:")
|
|
for prob_key, prob_value in value.items():
|
|
output_lines.append(f" {prob_key}: {prob_value:.3f}")
|
|
else:
|
|
output_lines.append(f" 📝 {key}: {value}")
|
|
else:
|
|
# Handle other result types
|
|
output_lines.append(f" 📝 Raw Result: {result}")
|
|
|
|
output_lines.append("") # Blank line between stages
|
|
else:
|
|
output_lines.append(" ⚠️ No stage results available")
|
|
|
|
# Processing time if available
|
|
metadata = result_dict.get('metadata', {})
|
|
if 'total_processing_time' in metadata:
|
|
processing_time = metadata['total_processing_time']
|
|
output_lines.append(f" ⏱️ Processing Time: {processing_time:.3f}s")
|
|
|
|
# Real-time FPS calculation based on output queue throughput
|
|
current_fps = result_dict.get('current_pipeline_fps', 0.0)
|
|
if current_fps > 0:
|
|
output_lines.append(f" 🚄 Pipeline FPS: {current_fps:.2f}")
|
|
else:
|
|
output_lines.append(f" 🚄 Pipeline FPS: Calculating...")
|
|
|
|
# Additional metadata
|
|
if metadata:
|
|
interesting_keys = ['dongle_count', 'stage_count', 'queue_sizes', 'error_count']
|
|
for key in interesting_keys:
|
|
if key in metadata:
|
|
output_lines.append(f" 📋 {key.replace('_', ' ').title()}: {metadata[key]}")
|
|
|
|
output_lines.append(" " + "="*50)
|
|
|
|
return "\n".join(output_lines)
|
|
|
|
except Exception as e:
|
|
return f"❌ Error formatting terminal results: {e}"
|
|
|
|
|
|
class DeploymentDialog(QDialog):
|
|
"""Main deployment dialog with comprehensive deployment management."""
|
|
|
|
def __init__(self, pipeline_data: Dict[str, Any], parent=None):
|
|
super().__init__(parent)
|
|
self.pipeline_data = pipeline_data
|
|
self.deployment_worker = None
|
|
self.pipeline_config = None
|
|
|
|
self.setWindowTitle("Deploy Pipeline to Dongles")
|
|
self.setMinimumSize(800, 600)
|
|
self.setup_ui()
|
|
self.apply_theme()
|
|
|
|
def setup_ui(self):
|
|
"""Setup the dialog UI."""
|
|
layout = QVBoxLayout(self)
|
|
|
|
# Header
|
|
header_label = QLabel("Pipeline Deployment")
|
|
header_label.setFont(QFont("Arial", 16, QFont.Bold))
|
|
header_label.setAlignment(Qt.AlignCenter)
|
|
layout.addWidget(header_label)
|
|
|
|
# Main content with tabs
|
|
self.tab_widget = QTabWidget()
|
|
|
|
# Overview tab
|
|
self.overview_tab = self.create_overview_tab()
|
|
self.tab_widget.addTab(self.overview_tab, "Overview")
|
|
|
|
# Topology tab
|
|
self.topology_tab = self.create_topology_tab()
|
|
self.tab_widget.addTab(self.topology_tab, "Topology Analysis")
|
|
|
|
# Configuration tab
|
|
self.config_tab = self.create_configuration_tab()
|
|
self.tab_widget.addTab(self.config_tab, "Configuration")
|
|
|
|
# Deployment tab
|
|
self.deployment_tab = self.create_deployment_tab()
|
|
self.tab_widget.addTab(self.deployment_tab, "Deployment")
|
|
|
|
# Live View tab
|
|
self.live_view_tab = self.create_live_view_tab()
|
|
self.tab_widget.addTab(self.live_view_tab, "Live View")
|
|
|
|
layout.addWidget(self.tab_widget)
|
|
|
|
# Progress bar
|
|
self.progress_bar = QProgressBar()
|
|
self.progress_bar.setVisible(False)
|
|
layout.addWidget(self.progress_bar)
|
|
|
|
# Status label
|
|
self.status_label = QLabel("Ready to deploy")
|
|
self.status_label.setAlignment(Qt.AlignCenter)
|
|
layout.addWidget(self.status_label)
|
|
|
|
# Buttons
|
|
button_layout = QHBoxLayout()
|
|
|
|
self.analyze_button = QPushButton("Analyze Pipeline")
|
|
self.analyze_button.clicked.connect(self.analyze_pipeline)
|
|
button_layout.addWidget(self.analyze_button)
|
|
|
|
self.deploy_button = QPushButton("Deploy to Dongles")
|
|
self.deploy_button.clicked.connect(self.start_deployment)
|
|
self.deploy_button.setEnabled(False)
|
|
button_layout.addWidget(self.deploy_button)
|
|
|
|
self.stop_button = QPushButton("Stop Inference")
|
|
self.stop_button.clicked.connect(self.stop_deployment)
|
|
self.stop_button.setEnabled(False)
|
|
self.stop_button.setVisible(False)
|
|
button_layout.addWidget(self.stop_button)
|
|
|
|
button_layout.addStretch()
|
|
|
|
self.close_button = QPushButton("Close")
|
|
self.close_button.clicked.connect(self.accept)
|
|
button_layout.addWidget(self.close_button)
|
|
|
|
layout.addLayout(button_layout)
|
|
|
|
# Populate initial data
|
|
self.populate_overview()
|
|
|
|
def create_overview_tab(self) -> QWidget:
|
|
"""Create pipeline overview tab."""
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
|
|
# Pipeline info
|
|
info_group = QGroupBox("Pipeline Information")
|
|
info_layout = QFormLayout(info_group)
|
|
|
|
self.name_label = QLabel()
|
|
self.description_label = QLabel()
|
|
self.nodes_label = QLabel()
|
|
self.connections_label = QLabel()
|
|
|
|
info_layout.addRow("Name:", self.name_label)
|
|
info_layout.addRow("Description:", self.description_label)
|
|
info_layout.addRow("Nodes:", self.nodes_label)
|
|
info_layout.addRow("Connections:", self.connections_label)
|
|
|
|
layout.addWidget(info_group)
|
|
|
|
# Nodes table
|
|
nodes_group = QGroupBox("Pipeline Nodes")
|
|
nodes_layout = QVBoxLayout(nodes_group)
|
|
|
|
self.nodes_table = QTableWidget()
|
|
self.nodes_table.setColumnCount(3)
|
|
self.nodes_table.setHorizontalHeaderLabels(["Name", "Type", "Status"])
|
|
self.nodes_table.horizontalHeader().setStretchLastSection(True)
|
|
nodes_layout.addWidget(self.nodes_table)
|
|
|
|
layout.addWidget(nodes_group)
|
|
|
|
return widget
|
|
|
|
def create_topology_tab(self) -> QWidget:
|
|
"""Create topology analysis tab."""
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
|
|
# Analysis results
|
|
self.topology_text = QTextEdit()
|
|
self.topology_text.setReadOnly(True)
|
|
self.topology_text.setFont(QFont("Consolas", 10))
|
|
self.topology_text.setText("Click 'Analyze Pipeline' to see topology analysis...")
|
|
|
|
layout.addWidget(self.topology_text)
|
|
|
|
return widget
|
|
|
|
def create_configuration_tab(self) -> QWidget:
|
|
"""Create configuration tab."""
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
|
|
scroll_area = QScrollArea()
|
|
scroll_content = QWidget()
|
|
scroll_layout = QVBoxLayout(scroll_content)
|
|
|
|
# Stage configurations will be populated after analysis
|
|
self.config_content = QLabel("Run pipeline analysis to see stage configurations...")
|
|
self.config_content.setAlignment(Qt.AlignCenter)
|
|
scroll_layout.addWidget(self.config_content)
|
|
|
|
scroll_area.setWidget(scroll_content)
|
|
scroll_area.setWidgetResizable(True)
|
|
layout.addWidget(scroll_area)
|
|
|
|
return widget
|
|
|
|
def create_deployment_tab(self) -> QWidget:
|
|
"""Create deployment monitoring tab."""
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
|
|
# Create splitter for deployment log and terminal output
|
|
splitter = QSplitter(Qt.Vertical)
|
|
|
|
# Deployment log
|
|
log_group = QGroupBox("Deployment Log")
|
|
log_layout = QVBoxLayout(log_group)
|
|
|
|
self.deployment_log = QTextEdit()
|
|
self.deployment_log.setReadOnly(True)
|
|
self.deployment_log.setFont(QFont("Consolas", 9))
|
|
self.deployment_log.setMaximumHeight(200)
|
|
log_layout.addWidget(self.deployment_log)
|
|
|
|
splitter.addWidget(log_group)
|
|
|
|
# Terminal output display
|
|
terminal_group = QGroupBox("Terminal Output")
|
|
terminal_layout = QVBoxLayout(terminal_group)
|
|
|
|
self.terminal_output_display = QTextEdit()
|
|
self.terminal_output_display.setReadOnly(True)
|
|
self.terminal_output_display.setFont(QFont("Consolas", 9))
|
|
self.terminal_output_display.setStyleSheet("""
|
|
QTextEdit {
|
|
background-color: #1e1e1e;
|
|
color: #ffffff;
|
|
font-family: 'Consolas', 'Monaco', monospace;
|
|
}
|
|
""")
|
|
terminal_layout.addWidget(self.terminal_output_display)
|
|
|
|
splitter.addWidget(terminal_group)
|
|
|
|
# Set splitter proportions (1:2 ratio - more space for terminal)
|
|
splitter.setSizes([200, 400])
|
|
|
|
layout.addWidget(splitter)
|
|
|
|
# Dongle status (placeholder)
|
|
status_group = QGroupBox("Dongle Status")
|
|
status_layout = QVBoxLayout(status_group)
|
|
|
|
self.dongle_status = QLabel("No dongles detected")
|
|
self.dongle_status.setAlignment(Qt.AlignCenter)
|
|
status_layout.addWidget(self.dongle_status)
|
|
|
|
layout.addWidget(status_group)
|
|
|
|
return widget
|
|
|
|
def create_live_view_tab(self) -> QWidget:
|
|
"""Create the live view tab for real-time output."""
|
|
widget = QWidget()
|
|
layout = QHBoxLayout(widget)
|
|
|
|
# Video display
|
|
video_group = QGroupBox("Live Video Feed")
|
|
video_layout = QVBoxLayout(video_group)
|
|
self.live_view_label = QLabel("Live view will appear here after deployment.")
|
|
self.live_view_label.setAlignment(Qt.AlignCenter)
|
|
self.live_view_label.setMinimumSize(640, 480)
|
|
video_layout.addWidget(self.live_view_label)
|
|
layout.addWidget(video_group, 2)
|
|
|
|
# Inference results
|
|
results_group = QGroupBox("Inference Results")
|
|
results_layout = QVBoxLayout(results_group)
|
|
self.results_text = QTextEdit()
|
|
self.results_text.setReadOnly(True)
|
|
results_layout.addWidget(self.results_text)
|
|
layout.addWidget(results_group, 1)
|
|
|
|
return widget
|
|
|
|
def populate_overview(self):
|
|
"""Populate overview tab with pipeline data."""
|
|
self.name_label.setText(self.pipeline_data.get('project_name', 'Untitled'))
|
|
self.description_label.setText(self.pipeline_data.get('description', 'No description'))
|
|
|
|
nodes = self.pipeline_data.get('nodes', [])
|
|
connections = self.pipeline_data.get('connections', [])
|
|
|
|
self.nodes_label.setText(str(len(nodes)))
|
|
self.connections_label.setText(str(len(connections)))
|
|
|
|
# Populate nodes table
|
|
self.nodes_table.setRowCount(len(nodes))
|
|
for i, node in enumerate(nodes):
|
|
self.nodes_table.setItem(i, 0, QTableWidgetItem(node.get('name', 'Unknown')))
|
|
self.nodes_table.setItem(i, 1, QTableWidgetItem(node.get('type', 'Unknown')))
|
|
self.nodes_table.setItem(i, 2, QTableWidgetItem("Ready"))
|
|
|
|
def analyze_pipeline(self):
|
|
"""Analyze pipeline topology and configuration."""
|
|
if not CONVERTER_AVAILABLE:
|
|
QMessageBox.warning(self, "Analysis Error",
|
|
"Pipeline analyzer not available. Please check installation.")
|
|
return
|
|
|
|
try:
|
|
self.status_label.setText("Analyzing pipeline...")
|
|
self.analyze_button.setEnabled(False)
|
|
|
|
# Create converter and analyze
|
|
converter = MFlowConverter()
|
|
config = converter._convert_mflow_to_config(self.pipeline_data)
|
|
self.pipeline_config = config
|
|
|
|
# Update topology tab
|
|
analysis_text = f"""Pipeline Analysis Results:
|
|
|
|
Name: {config.pipeline_name}
|
|
Description: {config.description}
|
|
Total Stages: {len(config.stage_configs)}
|
|
|
|
Input Configuration:
|
|
{json.dumps(config.input_config, indent=2)}
|
|
|
|
Output Configuration:
|
|
{json.dumps(config.output_config, indent=2)}
|
|
|
|
Stage Configurations:
|
|
"""
|
|
|
|
for i, stage_config in enumerate(config.stage_configs, 1):
|
|
analysis_text += f"\nStage {i}: {stage_config.stage_id}\n"
|
|
analysis_text += f" Port IDs: {stage_config.port_ids}\n"
|
|
analysis_text += f" Model Path: {stage_config.model_path}\n"
|
|
analysis_text += f" SCPU Firmware: {stage_config.scpu_fw_path}\n"
|
|
analysis_text += f" NCPU Firmware: {stage_config.ncpu_fw_path}\n"
|
|
analysis_text += f" Upload Firmware: {stage_config.upload_fw}\n"
|
|
analysis_text += f" Max Queue Size: {stage_config.max_queue_size}\n"
|
|
|
|
self.topology_text.setText(analysis_text)
|
|
|
|
# Update configuration tab
|
|
self.update_configuration_tab(config)
|
|
|
|
# Validate configuration
|
|
is_valid, errors = converter.validate_config(config)
|
|
|
|
if is_valid:
|
|
self.status_label.setText("Pipeline analysis completed successfully")
|
|
self.deploy_button.setEnabled(True)
|
|
self.tab_widget.setCurrentIndex(1) # Switch to topology tab
|
|
else:
|
|
error_msg = "Configuration validation failed:\n" + "\n".join(errors)
|
|
QMessageBox.warning(self, "Validation Error", error_msg)
|
|
self.status_label.setText("Pipeline analysis failed validation")
|
|
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Analysis Error",
|
|
f"Failed to analyze pipeline: {str(e)}")
|
|
self.status_label.setText("Pipeline analysis failed")
|
|
finally:
|
|
self.analyze_button.setEnabled(True)
|
|
|
|
def update_configuration_tab(self, config: 'PipelineConfig'):
|
|
"""Update configuration tab with detailed stage information."""
|
|
# Clear existing content
|
|
scroll_content = QWidget()
|
|
scroll_layout = QVBoxLayout(scroll_content)
|
|
|
|
for i, stage_config in enumerate(config.stage_configs, 1):
|
|
stage_group = QGroupBox(f"Stage {i}: {stage_config.stage_id}")
|
|
stage_layout = QFormLayout(stage_group)
|
|
|
|
# Create read-only fields for stage configuration
|
|
model_path_edit = QLineEdit(stage_config.model_path)
|
|
model_path_edit.setReadOnly(True)
|
|
stage_layout.addRow("Model Path:", model_path_edit)
|
|
|
|
scpu_fw_edit = QLineEdit(stage_config.scpu_fw_path)
|
|
scpu_fw_edit.setReadOnly(True)
|
|
stage_layout.addRow("SCPU Firmware:", scpu_fw_edit)
|
|
|
|
ncpu_fw_edit = QLineEdit(stage_config.ncpu_fw_path)
|
|
ncpu_fw_edit.setReadOnly(True)
|
|
stage_layout.addRow("NCPU Firmware:", ncpu_fw_edit)
|
|
|
|
port_ids_edit = QLineEdit(str(stage_config.port_ids))
|
|
port_ids_edit.setReadOnly(True)
|
|
stage_layout.addRow("Port IDs:", port_ids_edit)
|
|
|
|
queue_size_spin = QSpinBox()
|
|
queue_size_spin.setValue(stage_config.max_queue_size)
|
|
queue_size_spin.setReadOnly(True)
|
|
stage_layout.addRow("Queue Size:", queue_size_spin)
|
|
|
|
upload_fw_check = QCheckBox()
|
|
upload_fw_check.setChecked(stage_config.upload_fw)
|
|
upload_fw_check.setEnabled(False)
|
|
stage_layout.addRow("Upload Firmware:", upload_fw_check)
|
|
|
|
scroll_layout.addWidget(stage_group)
|
|
|
|
# Update the configuration tab
|
|
config_tab_layout = self.config_tab.layout()
|
|
old_scroll_area = config_tab_layout.itemAt(0).widget()
|
|
config_tab_layout.removeWidget(old_scroll_area)
|
|
old_scroll_area.deleteLater()
|
|
|
|
new_scroll_area = QScrollArea()
|
|
new_scroll_area.setWidget(scroll_content)
|
|
new_scroll_area.setWidgetResizable(True)
|
|
config_tab_layout.addWidget(new_scroll_area)
|
|
|
|
def start_deployment(self):
|
|
"""Start the deployment process."""
|
|
if not self.pipeline_config:
|
|
QMessageBox.warning(self, "Deployment Error",
|
|
"Please analyze the pipeline first.")
|
|
return
|
|
|
|
# Switch to deployment tab
|
|
self.tab_widget.setCurrentIndex(3)
|
|
|
|
# Setup UI for deployment
|
|
self.progress_bar.setVisible(True)
|
|
self.progress_bar.setValue(0)
|
|
self.deploy_button.setEnabled(False)
|
|
self.close_button.setText("Cancel")
|
|
|
|
# Clear deployment log and terminal output
|
|
self.deployment_log.clear()
|
|
self.deployment_log.append("Starting pipeline deployment...")
|
|
self.terminal_output_display.clear()
|
|
self.terminal_output_display.append("🚀 Pipeline deployment started - terminal output will appear here...")
|
|
|
|
# Create and start deployment worker
|
|
self.deployment_worker = DeploymentWorker(self.pipeline_data)
|
|
self.deployment_worker.progress_updated.connect(self.update_progress)
|
|
self.deployment_worker.topology_analyzed.connect(self.update_topology_results)
|
|
self.deployment_worker.conversion_completed.connect(self.on_conversion_completed)
|
|
self.deployment_worker.deployment_started.connect(self.on_deployment_started)
|
|
self.deployment_worker.deployment_completed.connect(self.on_deployment_completed)
|
|
self.deployment_worker.error_occurred.connect(self.on_deployment_error)
|
|
self.deployment_worker.frame_updated.connect(self.update_live_view)
|
|
self.deployment_worker.result_updated.connect(self.update_inference_results)
|
|
self.deployment_worker.terminal_output.connect(self.update_terminal_output)
|
|
self.deployment_worker.stdout_captured.connect(self.update_terminal_output)
|
|
|
|
|
|
self.deployment_worker.start()
|
|
|
|
def stop_deployment(self):
|
|
"""Stop the current deployment/inference."""
|
|
if self.deployment_worker and self.deployment_worker.isRunning():
|
|
reply = QMessageBox.question(self, "Stop Inference",
|
|
"Are you sure you want to stop the inference?",
|
|
QMessageBox.Yes | QMessageBox.No)
|
|
if reply == QMessageBox.Yes:
|
|
self.deployment_log.append("Stopping inference...")
|
|
self.status_label.setText("Stopping inference...")
|
|
|
|
# Disable stop button immediately to prevent multiple clicks
|
|
self.stop_button.setEnabled(False)
|
|
|
|
self.deployment_worker.stop()
|
|
|
|
# Wait for worker to finish in a separate thread to avoid blocking UI
|
|
def wait_for_stop():
|
|
if self.deployment_worker.wait(5000): # Wait up to 5 seconds
|
|
self.deployment_log.append("Inference stopped successfully.")
|
|
else:
|
|
self.deployment_log.append("Warning: Inference may not have stopped cleanly.")
|
|
|
|
# Update UI on main thread
|
|
self.stop_button.setVisible(False)
|
|
self.deploy_button.setEnabled(True)
|
|
self.close_button.setText("Close")
|
|
self.progress_bar.setVisible(False)
|
|
self.status_label.setText("Inference stopped")
|
|
self.dongle_status.setText("Pipeline stopped")
|
|
|
|
import threading
|
|
threading.Thread(target=wait_for_stop, daemon=True).start()
|
|
|
|
def update_progress(self, value: int, message: str):
|
|
"""Update deployment progress."""
|
|
self.progress_bar.setValue(value)
|
|
self.status_label.setText(message)
|
|
self.deployment_log.append(f"[{value}%] {message}")
|
|
|
|
def update_topology_results(self, results: Dict):
|
|
"""Update topology analysis results."""
|
|
self.deployment_log.append(f"Topology Analysis: {results['total_stages']} stages detected")
|
|
|
|
def on_conversion_completed(self, config):
|
|
"""Handle conversion completion."""
|
|
self.deployment_log.append("Pipeline conversion completed successfully")
|
|
|
|
def on_deployment_started(self):
|
|
"""Handle deployment start."""
|
|
self.deployment_log.append("Connecting to dongles...")
|
|
self.dongle_status.setText("Initializing dongles...")
|
|
|
|
# Show stop button and hide deploy button
|
|
self.stop_button.setEnabled(True)
|
|
self.stop_button.setVisible(True)
|
|
self.deploy_button.setEnabled(False)
|
|
|
|
def on_deployment_completed(self, success: bool, message: str):
|
|
"""Handle deployment completion."""
|
|
self.progress_bar.setValue(100)
|
|
|
|
if success:
|
|
self.deployment_log.append(f"SUCCESS: {message}")
|
|
self.status_label.setText("Deployment completed successfully!")
|
|
self.dongle_status.setText("Pipeline running on dongles")
|
|
# Keep stop button visible for successful deployment
|
|
self.stop_button.setEnabled(True)
|
|
self.stop_button.setVisible(True)
|
|
QMessageBox.information(self, "Deployment Success", message)
|
|
else:
|
|
self.deployment_log.append(f"FAILED: {message}")
|
|
self.status_label.setText("Deployment failed")
|
|
# Hide stop button for failed deployment
|
|
self.stop_button.setEnabled(False)
|
|
self.stop_button.setVisible(False)
|
|
self.deploy_button.setEnabled(True)
|
|
|
|
self.close_button.setText("Close")
|
|
self.progress_bar.setVisible(False)
|
|
|
|
def on_deployment_error(self, error: str):
|
|
"""Handle deployment error."""
|
|
self.deployment_log.append(f"ERROR: {error}")
|
|
self.status_label.setText("Deployment failed")
|
|
QMessageBox.critical(self, "Deployment Error", error)
|
|
|
|
# Hide stop button and show deploy button on error
|
|
self.stop_button.setEnabled(False)
|
|
self.stop_button.setVisible(False)
|
|
self.deploy_button.setEnabled(True)
|
|
self.close_button.setText("Close")
|
|
self.progress_bar.setVisible(False)
|
|
|
|
def update_live_view(self, frame):
|
|
"""Update the live view with a new frame."""
|
|
try:
|
|
# Convert the OpenCV frame to a QImage
|
|
height, width, channel = frame.shape
|
|
bytes_per_line = 3 * width
|
|
q_image = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888).rgbSwapped()
|
|
|
|
# Display the QImage in the QLabel
|
|
self.live_view_label.setPixmap(QPixmap.fromImage(q_image))
|
|
except Exception as e:
|
|
print(f"Error updating live view: {e}")
|
|
|
|
def update_inference_results(self, result_dict):
|
|
"""Update the inference results display."""
|
|
try:
|
|
import json
|
|
from datetime import datetime
|
|
|
|
# Format the results for display
|
|
timestamp = datetime.fromtimestamp(result_dict.get('timestamp', 0)).strftime("%H:%M:%S.%f")[:-3]
|
|
stage_results = result_dict.get('stage_results', {})
|
|
|
|
result_text = f"[{timestamp}] Pipeline ID: {result_dict.get('pipeline_id', 'Unknown')}\n"
|
|
|
|
# Display results from each stage
|
|
for stage_id, result in stage_results.items():
|
|
result_text += f" {stage_id}:\n"
|
|
if isinstance(result, tuple) and len(result) == 2:
|
|
# Handle tuple results (probability, result_string)
|
|
probability, result_string = result
|
|
result_text += f" Result: {result_string}\n"
|
|
result_text += f" Probability: {probability:.3f}\n"
|
|
elif isinstance(result, dict):
|
|
# Handle dict results
|
|
for key, value in result.items():
|
|
if key == 'probability':
|
|
result_text += f" Probability: {value:.3f}\n"
|
|
else:
|
|
result_text += f" {key}: {value}\n"
|
|
else:
|
|
result_text += f" {result}\n"
|
|
|
|
result_text += "-" * 50 + "\n"
|
|
|
|
# Append to results display (keep last 100 lines)
|
|
current_text = self.results_text.toPlainText()
|
|
lines = current_text.split('\n')
|
|
if len(lines) > 100:
|
|
lines = lines[-50:] # Keep last 50 lines
|
|
current_text = '\n'.join(lines)
|
|
|
|
self.results_text.setPlainText(current_text + result_text)
|
|
|
|
# Auto-scroll to bottom
|
|
scrollbar = self.results_text.verticalScrollBar()
|
|
scrollbar.setValue(scrollbar.maximum())
|
|
|
|
except Exception as e:
|
|
print(f"Error updating inference results: {e}")
|
|
|
|
def update_terminal_output(self, terminal_text: str):
|
|
"""Update the terminal output display with new text."""
|
|
try:
|
|
# Use append() instead of setPlainText() for better performance and no truncation
|
|
self.terminal_output_display.append(terminal_text.rstrip('\n'))
|
|
|
|
# Auto-scroll to bottom
|
|
scrollbar = self.terminal_output_display.verticalScrollBar()
|
|
scrollbar.setValue(scrollbar.maximum())
|
|
|
|
# Optional: Limit total lines to prevent excessive memory usage
|
|
# Only trim if we have way too many lines (e.g., > 1000)
|
|
document = self.terminal_output_display.document()
|
|
if document.lineCount() > 1000:
|
|
cursor = self.terminal_output_display.textCursor()
|
|
cursor.movePosition(cursor.Start)
|
|
cursor.movePosition(cursor.Down, cursor.KeepAnchor, 200) # Select first 200 lines
|
|
cursor.removeSelectedText()
|
|
|
|
except Exception as e:
|
|
print(f"Error updating terminal output: {e}")
|
|
|
|
def apply_theme(self):
|
|
"""Apply consistent theme to the dialog."""
|
|
self.setStyleSheet("""
|
|
QDialog {
|
|
background-color: #1e1e2e;
|
|
color: #cdd6f4;
|
|
}
|
|
QTabWidget::pane {
|
|
border: 1px solid #45475a;
|
|
background-color: #313244;
|
|
}
|
|
QTabWidget::tab-bar {
|
|
alignment: center;
|
|
}
|
|
QTabBar::tab {
|
|
background-color: #45475a;
|
|
color: #cdd6f4;
|
|
padding: 8px 16px;
|
|
margin-right: 2px;
|
|
border-top-left-radius: 4px;
|
|
border-top-right-radius: 4px;
|
|
}
|
|
QTabBar::tab:selected {
|
|
background-color: #89b4fa;
|
|
color: #1e1e2e;
|
|
}
|
|
QTabBar::tab:hover {
|
|
background-color: #585b70;
|
|
}
|
|
QGroupBox {
|
|
font-weight: bold;
|
|
border: 2px solid #45475a;
|
|
border-radius: 5px;
|
|
margin-top: 1ex;
|
|
padding-top: 5px;
|
|
}
|
|
QGroupBox::title {
|
|
subcontrol-origin: margin;
|
|
left: 10px;
|
|
padding: 0 10px 0 10px;
|
|
}
|
|
QPushButton {
|
|
background-color: #45475a;
|
|
color: #cdd6f4;
|
|
border: 1px solid #6c7086;
|
|
border-radius: 4px;
|
|
padding: 8px 16px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #585b70;
|
|
}
|
|
QPushButton:pressed {
|
|
background-color: #313244;
|
|
}
|
|
QPushButton:disabled {
|
|
background-color: #313244;
|
|
color: #6c7086;
|
|
}
|
|
QTextEdit, QLineEdit {
|
|
background-color: #313244;
|
|
color: #cdd6f4;
|
|
border: 1px solid #45475a;
|
|
border-radius: 4px;
|
|
padding: 4px;
|
|
}
|
|
QTableWidget {
|
|
background-color: #313244;
|
|
alternate-background-color: #45475a;
|
|
color: #cdd6f4;
|
|
border: 1px solid #45475a;
|
|
}
|
|
QProgressBar {
|
|
background-color: #313244;
|
|
border: 1px solid #45475a;
|
|
border-radius: 4px;
|
|
text-align: center;
|
|
}
|
|
QProgressBar::chunk {
|
|
background-color: #a6e3a1;
|
|
border-radius: 3px;
|
|
}
|
|
""")
|
|
|
|
def closeEvent(self, event):
|
|
"""Handle dialog close event."""
|
|
if self.deployment_worker and self.deployment_worker.isRunning():
|
|
reply = QMessageBox.question(self, "Cancel Deployment",
|
|
"Deployment is in progress. Are you sure you want to cancel?",
|
|
QMessageBox.Yes | QMessageBox.No)
|
|
if reply == QMessageBox.Yes:
|
|
self.deployment_worker.stop()
|
|
self.deployment_worker.wait(3000) # Wait up to 3 seconds
|
|
event.accept()
|
|
else:
|
|
event.ignore()
|
|
else:
|
|
event.accept() |