feat: Implement multi-series dongle support
This commit is contained in:
parent
d6c0b5eab5
commit
e2c55d993c
398
core/functions/multi_series_converter.py
Normal file
398
core/functions/multi_series_converter.py
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
"""
|
||||||
|
Multi-Series UI Bridge Converter
|
||||||
|
|
||||||
|
This module provides a simplified bridge between the UI pipeline data and the
|
||||||
|
MultiSeriesDongleManager system, making it easy to convert UI configurations
|
||||||
|
to working multi-series inference pipelines.
|
||||||
|
|
||||||
|
Key Features:
|
||||||
|
- Direct conversion from UI pipeline data to MultiSeriesDongleManager config
|
||||||
|
- Simplified interface for deployment system
|
||||||
|
- Automatic validation and configuration generation
|
||||||
|
- Support for both folder-based and individual file configurations
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from multi_series_converter import MultiSeriesConverter
|
||||||
|
|
||||||
|
converter = MultiSeriesConverter()
|
||||||
|
manager = converter.create_multi_series_manager(pipeline_data, ui_config)
|
||||||
|
|
||||||
|
manager.start()
|
||||||
|
sequence_id = manager.put_input(image, 'BGR565')
|
||||||
|
result = manager.get_result()
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import Dict, Any, List, Tuple, Optional
|
||||||
|
|
||||||
|
# Add parent directory to path for imports
|
||||||
|
current_dir = os.path.dirname(__file__)
|
||||||
|
parent_dir = os.path.dirname(os.path.dirname(current_dir))
|
||||||
|
sys.path.insert(0, parent_dir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from multi_series_dongle_manager import MultiSeriesDongleManager, DongleSeriesSpec
|
||||||
|
MULTI_SERIES_AVAILABLE = True
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"MultiSeriesDongleManager not available: {e}")
|
||||||
|
MULTI_SERIES_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
|
class MultiSeriesConverter:
|
||||||
|
"""Simplified converter for UI to MultiSeriesDongleManager bridge"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.series_specs = DongleSeriesSpec.SERIES_SPECS if MULTI_SERIES_AVAILABLE else {
|
||||||
|
0x100: {"name": "KL520", "gops": 3},
|
||||||
|
0x720: {"name": "KL720", "gops": 28},
|
||||||
|
0x630: {"name": "KL630", "gops": 400},
|
||||||
|
0x730: {"name": "KL730", "gops": 1600},
|
||||||
|
0x540: {"name": "KL540", "gops": 800}
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_multi_series_manager(self, pipeline_data: Dict[str, Any],
|
||||||
|
multi_series_config: Dict[str, Any]) -> Optional[MultiSeriesDongleManager]:
|
||||||
|
"""
|
||||||
|
Create and configure MultiSeriesDongleManager from UI data
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pipeline_data: Pipeline data from UI (.mflow format)
|
||||||
|
multi_series_config: Configuration from MultiSeriesConfigDialog
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Configured MultiSeriesDongleManager or None if creation fails
|
||||||
|
"""
|
||||||
|
if not MULTI_SERIES_AVAILABLE:
|
||||||
|
print("MultiSeriesDongleManager not available")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Extract firmware and model paths
|
||||||
|
firmware_paths, model_paths = self._extract_paths(multi_series_config)
|
||||||
|
|
||||||
|
if not firmware_paths or not model_paths:
|
||||||
|
print("Insufficient firmware or model paths")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Create and initialize manager
|
||||||
|
manager = MultiSeriesDongleManager(
|
||||||
|
max_queue_size=multi_series_config.get('max_queue_size', 100),
|
||||||
|
result_buffer_size=multi_series_config.get('result_buffer_size', 1000)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize devices
|
||||||
|
success = manager.scan_and_initialize_devices(firmware_paths, model_paths)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
print("Failed to initialize multi-series devices")
|
||||||
|
return None
|
||||||
|
|
||||||
|
print("Multi-series manager created and initialized successfully")
|
||||||
|
return manager
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating multi-series manager: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _extract_paths(self, multi_series_config: Dict[str, Any]) -> Tuple[Dict[str, Dict[str, str]], Dict[str, str]]:
|
||||||
|
"""Extract firmware and model paths from multi-series config"""
|
||||||
|
config_mode = multi_series_config.get('config_mode', 'folder')
|
||||||
|
enabled_series = multi_series_config.get('enabled_series', [])
|
||||||
|
|
||||||
|
firmware_paths = {}
|
||||||
|
model_paths = {}
|
||||||
|
|
||||||
|
if config_mode == 'folder':
|
||||||
|
firmware_paths, model_paths = self._extract_folder_paths(multi_series_config, enabled_series)
|
||||||
|
else:
|
||||||
|
firmware_paths, model_paths = self._extract_individual_paths(multi_series_config, enabled_series)
|
||||||
|
|
||||||
|
return firmware_paths, model_paths
|
||||||
|
|
||||||
|
def _extract_folder_paths(self, config: Dict[str, Any], enabled_series: List[str]) -> Tuple[Dict[str, Dict[str, str]], Dict[str, str]]:
|
||||||
|
"""Extract paths from folder-based configuration"""
|
||||||
|
assets_folder = config.get('assets_folder', '')
|
||||||
|
if not assets_folder or not os.path.exists(assets_folder):
|
||||||
|
print(f"Assets folder not found: {assets_folder}")
|
||||||
|
return {}, {}
|
||||||
|
|
||||||
|
firmware_base = os.path.join(assets_folder, 'Firmware')
|
||||||
|
models_base = os.path.join(assets_folder, 'Models')
|
||||||
|
|
||||||
|
firmware_paths = {}
|
||||||
|
model_paths = {}
|
||||||
|
|
||||||
|
for series in enabled_series:
|
||||||
|
series_name = f'KL{series}' if series.isdigit() else series
|
||||||
|
|
||||||
|
# Firmware paths
|
||||||
|
series_fw_dir = os.path.join(firmware_base, series_name)
|
||||||
|
if os.path.exists(series_fw_dir):
|
||||||
|
scpu_path = os.path.join(series_fw_dir, 'fw_scpu.bin')
|
||||||
|
ncpu_path = os.path.join(series_fw_dir, 'fw_ncpu.bin')
|
||||||
|
|
||||||
|
if os.path.exists(scpu_path) and os.path.exists(ncpu_path):
|
||||||
|
firmware_paths[series_name] = {
|
||||||
|
'scpu': scpu_path,
|
||||||
|
'ncpu': ncpu_path
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
print(f"Warning: Missing firmware files for {series_name}")
|
||||||
|
|
||||||
|
# Model paths - find first .nef file
|
||||||
|
series_model_dir = os.path.join(models_base, series_name)
|
||||||
|
if os.path.exists(series_model_dir):
|
||||||
|
model_files = [f for f in os.listdir(series_model_dir) if f.endswith('.nef')]
|
||||||
|
if model_files:
|
||||||
|
model_paths[series_name] = os.path.join(series_model_dir, model_files[0])
|
||||||
|
else:
|
||||||
|
print(f"Warning: No .nef model files found for {series_name}")
|
||||||
|
|
||||||
|
return firmware_paths, model_paths
|
||||||
|
|
||||||
|
def _extract_individual_paths(self, config: Dict[str, Any], enabled_series: List[str]) -> Tuple[Dict[str, Dict[str, str]], Dict[str, str]]:
|
||||||
|
"""Extract paths from individual file configuration"""
|
||||||
|
individual_paths = config.get('individual_paths', {})
|
||||||
|
|
||||||
|
firmware_paths = {}
|
||||||
|
model_paths = {}
|
||||||
|
|
||||||
|
for series in enabled_series:
|
||||||
|
series_name = f'KL{series}' if series.isdigit() else series
|
||||||
|
|
||||||
|
if series_name in individual_paths:
|
||||||
|
series_config = individual_paths[series_name]
|
||||||
|
|
||||||
|
# Firmware paths
|
||||||
|
scpu_path = series_config.get('scpu', '')
|
||||||
|
ncpu_path = series_config.get('ncpu', '')
|
||||||
|
|
||||||
|
if scpu_path and ncpu_path and os.path.exists(scpu_path) and os.path.exists(ncpu_path):
|
||||||
|
firmware_paths[series_name] = {
|
||||||
|
'scpu': scpu_path,
|
||||||
|
'ncpu': ncpu_path
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
print(f"Warning: Invalid firmware paths for {series_name}")
|
||||||
|
|
||||||
|
# Model path
|
||||||
|
model_path = series_config.get('model', '')
|
||||||
|
if model_path and os.path.exists(model_path):
|
||||||
|
model_paths[series_name] = model_path
|
||||||
|
else:
|
||||||
|
print(f"Warning: Invalid model path for {series_name}")
|
||||||
|
|
||||||
|
return firmware_paths, model_paths
|
||||||
|
|
||||||
|
def validate_multi_series_config(self, multi_series_config: Dict[str, Any]) -> Tuple[bool, List[str]]:
|
||||||
|
"""
|
||||||
|
Validate multi-series configuration
|
||||||
|
|
||||||
|
Args:
|
||||||
|
multi_series_config: Configuration to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (is_valid, list_of_issues)
|
||||||
|
"""
|
||||||
|
issues = []
|
||||||
|
|
||||||
|
# Check enabled series
|
||||||
|
enabled_series = multi_series_config.get('enabled_series', [])
|
||||||
|
if not enabled_series:
|
||||||
|
issues.append("No series enabled")
|
||||||
|
|
||||||
|
# Check configuration mode
|
||||||
|
config_mode = multi_series_config.get('config_mode', 'folder')
|
||||||
|
if config_mode not in ['folder', 'individual']:
|
||||||
|
issues.append("Invalid configuration mode")
|
||||||
|
|
||||||
|
# Validate paths
|
||||||
|
firmware_paths, model_paths = self._extract_paths(multi_series_config)
|
||||||
|
|
||||||
|
if not firmware_paths:
|
||||||
|
issues.append("No valid firmware paths found")
|
||||||
|
|
||||||
|
if not model_paths:
|
||||||
|
issues.append("No valid model paths found")
|
||||||
|
|
||||||
|
# Check if all enabled series have both firmware and models
|
||||||
|
for series in enabled_series:
|
||||||
|
series_name = f'KL{series}' if series.isdigit() else series
|
||||||
|
|
||||||
|
if series_name not in firmware_paths:
|
||||||
|
issues.append(f"Missing firmware for {series_name}")
|
||||||
|
|
||||||
|
if series_name not in model_paths:
|
||||||
|
issues.append(f"Missing model for {series_name}")
|
||||||
|
|
||||||
|
# Check port mapping
|
||||||
|
port_mapping = multi_series_config.get('port_mapping', {})
|
||||||
|
if not port_mapping:
|
||||||
|
issues.append("No port mappings configured")
|
||||||
|
|
||||||
|
return len(issues) == 0, issues
|
||||||
|
|
||||||
|
def generate_config_summary(self, multi_series_config: Dict[str, Any]) -> str:
|
||||||
|
"""Generate a human-readable summary of the configuration"""
|
||||||
|
enabled_series = multi_series_config.get('enabled_series', [])
|
||||||
|
config_mode = multi_series_config.get('config_mode', 'folder')
|
||||||
|
port_mapping = multi_series_config.get('port_mapping', {})
|
||||||
|
|
||||||
|
summary = ["Multi-Series Configuration Summary", "=" * 40, ""]
|
||||||
|
|
||||||
|
summary.append(f"Configuration Mode: {config_mode}")
|
||||||
|
summary.append(f"Enabled Series: {', '.join(enabled_series)}")
|
||||||
|
summary.append(f"Port Mappings: {len(port_mapping)}")
|
||||||
|
summary.append("")
|
||||||
|
|
||||||
|
# Firmware and model paths
|
||||||
|
firmware_paths, model_paths = self._extract_paths(multi_series_config)
|
||||||
|
|
||||||
|
summary.append("Firmware Configuration:")
|
||||||
|
for series, fw_config in firmware_paths.items():
|
||||||
|
summary.append(f" {series}:")
|
||||||
|
summary.append(f" SCPU: {fw_config.get('scpu', 'Not configured')}")
|
||||||
|
summary.append(f" NCPU: {fw_config.get('ncpu', 'Not configured')}")
|
||||||
|
summary.append("")
|
||||||
|
|
||||||
|
summary.append("Model Configuration:")
|
||||||
|
for series, model_path in model_paths.items():
|
||||||
|
model_name = os.path.basename(model_path) if model_path else "Not configured"
|
||||||
|
summary.append(f" {series}: {model_name}")
|
||||||
|
summary.append("")
|
||||||
|
|
||||||
|
# Port mapping
|
||||||
|
summary.append("Port Mapping:")
|
||||||
|
if port_mapping:
|
||||||
|
for port_id, series in port_mapping.items():
|
||||||
|
summary.append(f" Port {port_id}: {series}")
|
||||||
|
else:
|
||||||
|
summary.append(" No port mappings configured")
|
||||||
|
|
||||||
|
return "\n".join(summary)
|
||||||
|
|
||||||
|
def get_performance_estimate(self, multi_series_config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Get estimated performance for the multi-series configuration"""
|
||||||
|
enabled_series = multi_series_config.get('enabled_series', [])
|
||||||
|
port_mapping = multi_series_config.get('port_mapping', {})
|
||||||
|
|
||||||
|
total_gops = 0
|
||||||
|
series_counts = {}
|
||||||
|
|
||||||
|
# Count devices per series
|
||||||
|
for port_id, series in port_mapping.items():
|
||||||
|
series_name = f'KL{series}' if series.isdigit() else series
|
||||||
|
series_counts[series_name] = series_counts.get(series_name, 0) + 1
|
||||||
|
|
||||||
|
# Calculate total GOPS
|
||||||
|
for series_name, count in series_counts.items():
|
||||||
|
# Find corresponding product_id
|
||||||
|
for product_id, spec in self.series_specs.items():
|
||||||
|
if spec["name"] == series_name:
|
||||||
|
gops = spec["gops"] * count
|
||||||
|
total_gops += gops
|
||||||
|
break
|
||||||
|
|
||||||
|
# Estimate FPS improvement
|
||||||
|
base_fps = 10 # Baseline single dongle FPS
|
||||||
|
estimated_fps = min(base_fps * (total_gops / 10), base_fps * 5) # Cap at 5x improvement
|
||||||
|
|
||||||
|
return {
|
||||||
|
'total_gops': total_gops,
|
||||||
|
'estimated_fps': estimated_fps,
|
||||||
|
'series_counts': series_counts,
|
||||||
|
'total_devices': len(port_mapping),
|
||||||
|
'load_balancing': 'automatic_by_gops'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Convenience function for easy usage
|
||||||
|
def create_multi_series_manager_from_ui(pipeline_data: Dict[str, Any],
|
||||||
|
multi_series_config: Dict[str, Any]) -> Optional[MultiSeriesDongleManager]:
|
||||||
|
"""
|
||||||
|
Convenience function to create MultiSeriesDongleManager from UI data
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pipeline_data: Pipeline data from UI (.mflow format)
|
||||||
|
multi_series_config: Configuration from MultiSeriesConfigDialog
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Configured MultiSeriesDongleManager or None if creation fails
|
||||||
|
"""
|
||||||
|
converter = MultiSeriesConverter()
|
||||||
|
return converter.create_multi_series_manager(pipeline_data, multi_series_config)
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage and testing
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Example configuration for testing
|
||||||
|
example_multi_series_config = {
|
||||||
|
'language': 'en',
|
||||||
|
'enabled_series': ['KL520', 'KL720'],
|
||||||
|
'config_mode': 'folder',
|
||||||
|
'assets_folder': r'C:\MyProject\Assets',
|
||||||
|
'port_mapping': {
|
||||||
|
28: 'KL520',
|
||||||
|
32: 'KL720'
|
||||||
|
},
|
||||||
|
'max_queue_size': 100,
|
||||||
|
'result_buffer_size': 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
example_pipeline_data = {
|
||||||
|
'project_name': 'Test Multi-Series Pipeline',
|
||||||
|
'description': 'Testing multi-series configuration',
|
||||||
|
'nodes': [
|
||||||
|
{'id': '1', 'type': 'input', 'name': 'Camera Input'},
|
||||||
|
{'id': '2', 'type': 'model', 'name': 'Detection Model',
|
||||||
|
'custom_properties': {'multi_series_mode': True}},
|
||||||
|
{'id': '3', 'type': 'output', 'name': 'Display Output'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
converter = MultiSeriesConverter()
|
||||||
|
|
||||||
|
# Validate configuration
|
||||||
|
is_valid, issues = converter.validate_multi_series_config(example_multi_series_config)
|
||||||
|
|
||||||
|
print("Multi-Series Converter Test")
|
||||||
|
print("=" * 30)
|
||||||
|
print(f"Configuration valid: {is_valid}")
|
||||||
|
|
||||||
|
if issues:
|
||||||
|
print("Issues found:")
|
||||||
|
for issue in issues:
|
||||||
|
print(f" - {issue}")
|
||||||
|
|
||||||
|
# Generate summary
|
||||||
|
print("\nConfiguration Summary:")
|
||||||
|
print(converter.generate_config_summary(example_multi_series_config))
|
||||||
|
|
||||||
|
# Get performance estimate
|
||||||
|
performance = converter.get_performance_estimate(example_multi_series_config)
|
||||||
|
print(f"\nPerformance Estimate:")
|
||||||
|
print(f" Total GOPS: {performance['total_gops']}")
|
||||||
|
print(f" Estimated FPS: {performance['estimated_fps']:.1f}")
|
||||||
|
print(f" Total devices: {performance['total_devices']}")
|
||||||
|
|
||||||
|
# Try to create manager (will fail without hardware)
|
||||||
|
if MULTI_SERIES_AVAILABLE:
|
||||||
|
manager = converter.create_multi_series_manager(
|
||||||
|
example_pipeline_data,
|
||||||
|
example_multi_series_config
|
||||||
|
)
|
||||||
|
|
||||||
|
if manager:
|
||||||
|
print("\n✓ MultiSeriesDongleManager created successfully")
|
||||||
|
manager.stop() # Clean shutdown
|
||||||
|
else:
|
||||||
|
print("\n✗ Failed to create MultiSeriesDongleManager (expected without hardware)")
|
||||||
|
else:
|
||||||
|
print("\n⚠ MultiSeriesDongleManager not available")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error testing multi-series converter: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
443
core/functions/multi_series_mflow_converter.py
Normal file
443
core/functions/multi_series_mflow_converter.py
Normal file
@ -0,0 +1,443 @@
|
|||||||
|
"""
|
||||||
|
Enhanced MFlow to Multi-Series API Converter
|
||||||
|
|
||||||
|
This module extends the MFlowConverter to support multi-series dongle configurations
|
||||||
|
by detecting multi-series model nodes and generating appropriate configurations for
|
||||||
|
the MultiSeriesDongleManager.
|
||||||
|
|
||||||
|
Key Features:
|
||||||
|
- Detect multi-series enabled model nodes
|
||||||
|
- Generate MultiSeriesStageConfig objects
|
||||||
|
- Maintain backward compatibility with single-series configurations
|
||||||
|
- Validate multi-series folder structures
|
||||||
|
- Optimize pipeline for mixed single/multi-series stages
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from multi_series_mflow_converter import MultiSeriesMFlowConverter
|
||||||
|
|
||||||
|
converter = MultiSeriesMFlowConverter()
|
||||||
|
pipeline_config = converter.load_and_convert("pipeline.mflow")
|
||||||
|
|
||||||
|
# Automatically creates appropriate pipeline type
|
||||||
|
if pipeline_config.has_multi_series:
|
||||||
|
pipeline = MultiSeriesInferencePipeline(pipeline_config.stage_configs)
|
||||||
|
else:
|
||||||
|
pipeline = InferencePipeline(pipeline_config.stage_configs)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import List, Dict, Any, Tuple, Union
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
# Import base converter and pipeline components
|
||||||
|
from .mflow_converter import MFlowConverter, PipelineConfig
|
||||||
|
from .multi_series_pipeline import MultiSeriesStageConfig, MultiSeriesInferencePipeline
|
||||||
|
from .InferencePipeline import StageConfig
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EnhancedPipelineConfig:
|
||||||
|
"""Enhanced pipeline configuration supporting both single and multi-series"""
|
||||||
|
stage_configs: List[Union[StageConfig, MultiSeriesStageConfig]]
|
||||||
|
pipeline_name: str
|
||||||
|
description: str
|
||||||
|
input_config: Dict[str, Any]
|
||||||
|
output_config: Dict[str, Any]
|
||||||
|
preprocessing_configs: List[Dict[str, Any]]
|
||||||
|
postprocessing_configs: List[Dict[str, Any]]
|
||||||
|
has_multi_series: bool = False
|
||||||
|
multi_series_count: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class MultiSeriesMFlowConverter(MFlowConverter):
|
||||||
|
"""Enhanced converter supporting multi-series configurations"""
|
||||||
|
|
||||||
|
def __init__(self, default_fw_path: str = "./firmware", default_assets_path: str = "./assets"):
|
||||||
|
"""
|
||||||
|
Initialize enhanced converter
|
||||||
|
|
||||||
|
Args:
|
||||||
|
default_fw_path: Default path for single-series firmware files
|
||||||
|
default_assets_path: Default path for multi-series assets folder structure
|
||||||
|
"""
|
||||||
|
super().__init__(default_fw_path)
|
||||||
|
self.default_assets_path = default_assets_path
|
||||||
|
|
||||||
|
def load_and_convert(self, mflow_file_path: str) -> EnhancedPipelineConfig:
|
||||||
|
"""
|
||||||
|
Load .mflow file and convert to enhanced API configuration
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mflow_file_path: Path to the .mflow file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
EnhancedPipelineConfig: Configuration supporting both single and multi-series
|
||||||
|
"""
|
||||||
|
with open(mflow_file_path, 'r') as f:
|
||||||
|
mflow_data = json.load(f)
|
||||||
|
|
||||||
|
return self._convert_mflow_to_enhanced_config(mflow_data)
|
||||||
|
|
||||||
|
def _convert_mflow_to_enhanced_config(self, mflow_data: Dict[str, Any]) -> EnhancedPipelineConfig:
|
||||||
|
"""Convert loaded .mflow data to EnhancedPipelineConfig"""
|
||||||
|
|
||||||
|
# Extract basic metadata
|
||||||
|
pipeline_name = mflow_data.get('project_name', 'Enhanced Pipeline')
|
||||||
|
description = mflow_data.get('description', '')
|
||||||
|
nodes = mflow_data.get('nodes', [])
|
||||||
|
connections = mflow_data.get('connections', [])
|
||||||
|
|
||||||
|
# Build node lookup and categorize nodes
|
||||||
|
self._build_node_map(nodes)
|
||||||
|
model_nodes, input_nodes, output_nodes, preprocess_nodes, postprocess_nodes = self._categorize_nodes()
|
||||||
|
|
||||||
|
# Determine stage order based on connections
|
||||||
|
self._determine_stage_order(model_nodes, connections)
|
||||||
|
|
||||||
|
# Create enhanced stage configs (supporting both single and multi-series)
|
||||||
|
stage_configs, has_multi_series, multi_series_count = self._create_enhanced_stage_configs(
|
||||||
|
model_nodes, preprocess_nodes, postprocess_nodes, connections
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract input/output configurations
|
||||||
|
input_config = self._extract_input_config(input_nodes)
|
||||||
|
output_config = self._extract_output_config(output_nodes)
|
||||||
|
|
||||||
|
# Extract preprocessing/postprocessing configurations
|
||||||
|
preprocessing_configs = self._extract_preprocessing_configs(preprocess_nodes)
|
||||||
|
postprocessing_configs = self._extract_postprocessing_configs(postprocess_nodes)
|
||||||
|
|
||||||
|
return EnhancedPipelineConfig(
|
||||||
|
stage_configs=stage_configs,
|
||||||
|
pipeline_name=pipeline_name,
|
||||||
|
description=description,
|
||||||
|
input_config=input_config,
|
||||||
|
output_config=output_config,
|
||||||
|
preprocessing_configs=preprocessing_configs,
|
||||||
|
postprocessing_configs=postprocessing_configs,
|
||||||
|
has_multi_series=has_multi_series,
|
||||||
|
multi_series_count=multi_series_count
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_enhanced_stage_configs(self, model_nodes: List[Dict], preprocess_nodes: List[Dict],
|
||||||
|
postprocess_nodes: List[Dict], connections: List[Dict]
|
||||||
|
) -> Tuple[List[Union[StageConfig, MultiSeriesStageConfig]], bool, int]:
|
||||||
|
"""
|
||||||
|
Create stage configurations supporting both single and multi-series modes
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (stage_configs, has_multi_series, multi_series_count)
|
||||||
|
"""
|
||||||
|
stage_configs = []
|
||||||
|
has_multi_series = False
|
||||||
|
multi_series_count = 0
|
||||||
|
|
||||||
|
for node in self.stage_order:
|
||||||
|
# Extract node properties - check both 'custom_properties' and 'custom' keys for compatibility
|
||||||
|
node_properties = node.get('custom_properties', {})
|
||||||
|
if not node_properties:
|
||||||
|
node_properties = node.get('custom', {})
|
||||||
|
|
||||||
|
# Check if this node is configured for multi-series mode
|
||||||
|
if node_properties.get('multi_series_mode', False):
|
||||||
|
# Create multi-series stage config
|
||||||
|
stage_config = self._create_multi_series_stage_config(node, preprocess_nodes, postprocess_nodes, connections)
|
||||||
|
stage_configs.append(stage_config)
|
||||||
|
has_multi_series = True
|
||||||
|
multi_series_count += 1
|
||||||
|
print(f"Created multi-series stage config for node: {node.get('name', 'Unknown')}")
|
||||||
|
else:
|
||||||
|
# Create single-series stage config (backward compatibility)
|
||||||
|
stage_config = self._create_single_series_stage_config(node, preprocess_nodes, postprocess_nodes, connections)
|
||||||
|
stage_configs.append(stage_config)
|
||||||
|
print(f"Created single-series stage config for node: {node.get('name', 'Unknown')}")
|
||||||
|
|
||||||
|
return stage_configs, has_multi_series, multi_series_count
|
||||||
|
|
||||||
|
def _create_multi_series_stage_config(self, node: Dict, preprocess_nodes: List[Dict],
|
||||||
|
postprocess_nodes: List[Dict], connections: List[Dict]) -> MultiSeriesStageConfig:
|
||||||
|
"""Create multi-series stage configuration from model node"""
|
||||||
|
|
||||||
|
# Extract node properties - check both 'custom_properties' and 'custom' keys for compatibility
|
||||||
|
node_properties = node.get('custom_properties', {})
|
||||||
|
if not node_properties:
|
||||||
|
node_properties = node.get('custom', {})
|
||||||
|
|
||||||
|
stage_id = node.get('name', f"stage_{node.get('id', 'unknown')}")
|
||||||
|
|
||||||
|
# Extract assets folder and validate structure
|
||||||
|
assets_folder = node_properties.get('assets_folder', '')
|
||||||
|
if not assets_folder or not os.path.exists(assets_folder):
|
||||||
|
raise ValueError(f"Multi-series assets folder not found or not specified for node {stage_id}: {assets_folder}")
|
||||||
|
|
||||||
|
# Get enabled series
|
||||||
|
enabled_series = node_properties.get('enabled_series', ['520', '720'])
|
||||||
|
if not enabled_series:
|
||||||
|
raise ValueError(f"No series enabled for multi-series node {stage_id}")
|
||||||
|
|
||||||
|
# Build firmware and model paths
|
||||||
|
firmware_paths = {}
|
||||||
|
model_paths = {}
|
||||||
|
|
||||||
|
firmware_folder = os.path.join(assets_folder, 'Firmware')
|
||||||
|
models_folder = os.path.join(assets_folder, 'Models')
|
||||||
|
|
||||||
|
for series in enabled_series:
|
||||||
|
series_name = f'KL{series}'
|
||||||
|
|
||||||
|
# Firmware paths
|
||||||
|
series_fw_folder = os.path.join(firmware_folder, series_name)
|
||||||
|
if os.path.exists(series_fw_folder):
|
||||||
|
firmware_paths[series_name] = {
|
||||||
|
'scpu': os.path.join(series_fw_folder, 'fw_scpu.bin'),
|
||||||
|
'ncpu': os.path.join(series_fw_folder, 'fw_ncpu.bin')
|
||||||
|
}
|
||||||
|
|
||||||
|
# Model paths - find the first .nef file
|
||||||
|
series_model_folder = os.path.join(models_folder, series_name)
|
||||||
|
if os.path.exists(series_model_folder):
|
||||||
|
model_files = [f for f in os.listdir(series_model_folder) if f.endswith('.nef')]
|
||||||
|
if model_files:
|
||||||
|
model_paths[series_name] = os.path.join(series_model_folder, model_files[0])
|
||||||
|
|
||||||
|
# Validate paths
|
||||||
|
if not firmware_paths:
|
||||||
|
raise ValueError(f"No firmware found for multi-series node {stage_id} in enabled series: {enabled_series}")
|
||||||
|
|
||||||
|
if not model_paths:
|
||||||
|
raise ValueError(f"No models found for multi-series node {stage_id} in enabled series: {enabled_series}")
|
||||||
|
|
||||||
|
return MultiSeriesStageConfig(
|
||||||
|
stage_id=stage_id,
|
||||||
|
multi_series_mode=True,
|
||||||
|
firmware_paths=firmware_paths,
|
||||||
|
model_paths=model_paths,
|
||||||
|
max_queue_size=node_properties.get('max_queue_size', 100),
|
||||||
|
result_buffer_size=node_properties.get('result_buffer_size', 1000),
|
||||||
|
# TODO: Add preprocessor/postprocessor support if needed
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_single_series_stage_config(self, node: Dict, preprocess_nodes: List[Dict],
|
||||||
|
postprocess_nodes: List[Dict], connections: List[Dict]) -> MultiSeriesStageConfig:
|
||||||
|
"""Create single-series stage configuration for backward compatibility"""
|
||||||
|
|
||||||
|
# Extract node properties - check both 'custom_properties' and 'custom' keys for compatibility
|
||||||
|
node_properties = node.get('custom_properties', {})
|
||||||
|
if not node_properties:
|
||||||
|
node_properties = node.get('custom', {})
|
||||||
|
|
||||||
|
stage_id = node.get('name', f"stage_{node.get('id', 'unknown')}")
|
||||||
|
|
||||||
|
# Extract single-series paths
|
||||||
|
model_path = node_properties.get('model_path', '')
|
||||||
|
scpu_fw_path = node_properties.get('scpu_fw_path', '')
|
||||||
|
ncpu_fw_path = node_properties.get('ncpu_fw_path', '')
|
||||||
|
|
||||||
|
# Validate single-series configuration
|
||||||
|
if not model_path:
|
||||||
|
raise ValueError(f"Model path required for single-series node {stage_id}")
|
||||||
|
|
||||||
|
return MultiSeriesStageConfig(
|
||||||
|
stage_id=stage_id,
|
||||||
|
multi_series_mode=False,
|
||||||
|
port_ids=[], # Will be auto-detected
|
||||||
|
scpu_fw_path=scpu_fw_path,
|
||||||
|
ncpu_fw_path=ncpu_fw_path,
|
||||||
|
model_path=model_path,
|
||||||
|
upload_fw=True if scpu_fw_path and ncpu_fw_path else False,
|
||||||
|
max_queue_size=node_properties.get('max_queue_size', 50),
|
||||||
|
# TODO: Add preprocessor/postprocessor support if needed
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_enhanced_config(self, config: EnhancedPipelineConfig) -> Tuple[bool, List[str]]:
|
||||||
|
"""
|
||||||
|
Validate enhanced pipeline configuration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (is_valid, list_of_error_messages)
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# Basic validation
|
||||||
|
if not config.stage_configs:
|
||||||
|
errors.append("No stages configured")
|
||||||
|
|
||||||
|
if not config.pipeline_name:
|
||||||
|
errors.append("Pipeline name is required")
|
||||||
|
|
||||||
|
# Validate each stage
|
||||||
|
for i, stage_config in enumerate(config.stage_configs):
|
||||||
|
stage_errors = self._validate_stage_config(stage_config, i)
|
||||||
|
errors.extend(stage_errors)
|
||||||
|
|
||||||
|
# Multi-series specific validation
|
||||||
|
if config.has_multi_series:
|
||||||
|
multi_series_errors = self._validate_multi_series_configuration(config)
|
||||||
|
errors.extend(multi_series_errors)
|
||||||
|
|
||||||
|
return len(errors) == 0, errors
|
||||||
|
|
||||||
|
def _validate_stage_config(self, stage_config: Union[StageConfig, MultiSeriesStageConfig], stage_index: int) -> List[str]:
|
||||||
|
"""Validate individual stage configuration"""
|
||||||
|
errors = []
|
||||||
|
stage_name = getattr(stage_config, 'stage_id', f'Stage {stage_index}')
|
||||||
|
|
||||||
|
if isinstance(stage_config, MultiSeriesStageConfig):
|
||||||
|
if stage_config.multi_series_mode:
|
||||||
|
# Validate multi-series configuration
|
||||||
|
if not stage_config.firmware_paths:
|
||||||
|
errors.append(f"{stage_name}: No firmware paths configured for multi-series mode")
|
||||||
|
|
||||||
|
if not stage_config.model_paths:
|
||||||
|
errors.append(f"{stage_name}: No model paths configured for multi-series mode")
|
||||||
|
|
||||||
|
# Validate file existence
|
||||||
|
for series_name, fw_paths in (stage_config.firmware_paths or {}).items():
|
||||||
|
scpu_path = fw_paths.get('scpu')
|
||||||
|
ncpu_path = fw_paths.get('ncpu')
|
||||||
|
|
||||||
|
if not scpu_path or not os.path.exists(scpu_path):
|
||||||
|
errors.append(f"{stage_name}: SCPU firmware not found for {series_name}: {scpu_path}")
|
||||||
|
|
||||||
|
if not ncpu_path or not os.path.exists(ncpu_path):
|
||||||
|
errors.append(f"{stage_name}: NCPU firmware not found for {series_name}: {ncpu_path}")
|
||||||
|
|
||||||
|
for series_name, model_path in (stage_config.model_paths or {}).items():
|
||||||
|
if not model_path or not os.path.exists(model_path):
|
||||||
|
errors.append(f"{stage_name}: Model not found for {series_name}: {model_path}")
|
||||||
|
else:
|
||||||
|
# Validate single-series configuration
|
||||||
|
if not stage_config.model_path:
|
||||||
|
errors.append(f"{stage_name}: Model path is required for single-series mode")
|
||||||
|
elif not os.path.exists(stage_config.model_path):
|
||||||
|
errors.append(f"{stage_name}: Model file not found: {stage_config.model_path}")
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
def _validate_multi_series_configuration(self, config: EnhancedPipelineConfig) -> List[str]:
|
||||||
|
"""Validate multi-series specific requirements"""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# Check for mixed configurations
|
||||||
|
single_series_count = len(config.stage_configs) - config.multi_series_count
|
||||||
|
|
||||||
|
if config.multi_series_count > 0 and single_series_count > 0:
|
||||||
|
# Mixed pipeline - add warning
|
||||||
|
print(f"Warning: Mixed pipeline detected - {config.multi_series_count} multi-series stages and {single_series_count} single-series stages")
|
||||||
|
|
||||||
|
# Additional multi-series validations can be added here
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
def create_enhanced_inference_pipeline(self, config: EnhancedPipelineConfig) -> Union[MultiSeriesInferencePipeline, 'InferencePipeline']:
|
||||||
|
"""
|
||||||
|
Create appropriate inference pipeline based on configuration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MultiSeriesInferencePipeline if multi-series stages detected, otherwise regular InferencePipeline
|
||||||
|
"""
|
||||||
|
if config.has_multi_series:
|
||||||
|
print(f"Creating MultiSeriesInferencePipeline with {config.multi_series_count} multi-series stages")
|
||||||
|
return MultiSeriesInferencePipeline(
|
||||||
|
stage_configs=config.stage_configs,
|
||||||
|
pipeline_name=config.pipeline_name
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("Creating standard InferencePipeline (single-series only)")
|
||||||
|
# Convert to standard StageConfig objects for backward compatibility
|
||||||
|
from .InferencePipeline import InferencePipeline
|
||||||
|
standard_configs = []
|
||||||
|
|
||||||
|
for stage_config in config.stage_configs:
|
||||||
|
if isinstance(stage_config, MultiSeriesStageConfig) and not stage_config.multi_series_mode:
|
||||||
|
# Convert to standard StageConfig
|
||||||
|
standard_config = StageConfig(
|
||||||
|
stage_id=stage_config.stage_id,
|
||||||
|
port_ids=stage_config.port_ids or [],
|
||||||
|
scpu_fw_path=stage_config.scpu_fw_path or '',
|
||||||
|
ncpu_fw_path=stage_config.ncpu_fw_path or '',
|
||||||
|
model_path=stage_config.model_path or '',
|
||||||
|
upload_fw=stage_config.upload_fw,
|
||||||
|
max_queue_size=stage_config.max_queue_size
|
||||||
|
)
|
||||||
|
standard_configs.append(standard_config)
|
||||||
|
|
||||||
|
return InferencePipeline(
|
||||||
|
stage_configs=standard_configs,
|
||||||
|
pipeline_name=config.pipeline_name
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_assets_folder_structure(base_path: str, series_list: List[str] = None):
|
||||||
|
"""
|
||||||
|
Create the recommended folder structure for multi-series assets
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_path: Root path where assets folder should be created
|
||||||
|
series_list: List of series to create folders for (default: ['520', '720', '630', '730', '540'])
|
||||||
|
"""
|
||||||
|
if series_list is None:
|
||||||
|
series_list = ['520', '720', '630', '730', '540']
|
||||||
|
|
||||||
|
assets_path = os.path.join(base_path, 'Assets')
|
||||||
|
firmware_path = os.path.join(assets_path, 'Firmware')
|
||||||
|
models_path = os.path.join(assets_path, 'Models')
|
||||||
|
|
||||||
|
# Create main directories
|
||||||
|
os.makedirs(firmware_path, exist_ok=True)
|
||||||
|
os.makedirs(models_path, exist_ok=True)
|
||||||
|
|
||||||
|
# Create series-specific directories
|
||||||
|
for series in series_list:
|
||||||
|
series_name = f'KL{series}'
|
||||||
|
os.makedirs(os.path.join(firmware_path, series_name), exist_ok=True)
|
||||||
|
os.makedirs(os.path.join(models_path, series_name), exist_ok=True)
|
||||||
|
|
||||||
|
# Create README file explaining the structure
|
||||||
|
readme_content = """
|
||||||
|
# Multi-Series Assets Folder Structure
|
||||||
|
|
||||||
|
This folder contains firmware and models organized by dongle series for multi-series inference.
|
||||||
|
|
||||||
|
## Structure:
|
||||||
|
```
|
||||||
|
Assets/
|
||||||
|
├── Firmware/
|
||||||
|
│ ├── KL520/
|
||||||
|
│ │ ├── fw_scpu.bin
|
||||||
|
│ │ └── fw_ncpu.bin
|
||||||
|
│ ├── KL720/
|
||||||
|
│ │ ├── fw_scpu.bin
|
||||||
|
│ │ └── fw_ncpu.bin
|
||||||
|
│ └── [other series...]
|
||||||
|
└── Models/
|
||||||
|
├── KL520/
|
||||||
|
│ └── [model.nef files]
|
||||||
|
├── KL720/
|
||||||
|
│ └── [model.nef files]
|
||||||
|
└── [other series...]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage:
|
||||||
|
1. Place firmware files (fw_scpu.bin, fw_ncpu.bin) in the appropriate series subfolder under Firmware/
|
||||||
|
2. Place model files (.nef) in the appropriate series subfolder under Models/
|
||||||
|
3. Configure your model node to use this Assets folder in multi-series mode
|
||||||
|
4. Select which series to enable in the model node properties
|
||||||
|
|
||||||
|
## Supported Series:
|
||||||
|
- KL520: Entry-level performance
|
||||||
|
- KL720: Mid-range performance
|
||||||
|
- KL630: High performance
|
||||||
|
- KL730: Very high performance
|
||||||
|
- KL540: Specialized performance
|
||||||
|
|
||||||
|
The multi-series system will automatically load balance inference across all enabled series
|
||||||
|
based on their GOPS capacity for optimal performance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open(os.path.join(assets_path, 'README.md'), 'w') as f:
|
||||||
|
f.write(readme_content.strip())
|
||||||
|
|
||||||
|
print(f"Multi-series assets folder structure created at: {assets_path}")
|
||||||
|
print("Please copy your firmware and model files to the appropriate series subfolders.")
|
||||||
433
core/functions/multi_series_pipeline.py
Normal file
433
core/functions/multi_series_pipeline.py
Normal file
@ -0,0 +1,433 @@
|
|||||||
|
"""
|
||||||
|
Multi-Series Inference Pipeline
|
||||||
|
|
||||||
|
This module extends the InferencePipeline to support multi-series dongle configurations
|
||||||
|
using the MultiSeriesDongleManager for improved performance across different dongle series.
|
||||||
|
|
||||||
|
Main Components:
|
||||||
|
- MultiSeriesPipelineStage: Pipeline stage supporting both single and multi-series modes
|
||||||
|
- Enhanced InferencePipeline with multi-series support
|
||||||
|
- Configuration adapters for seamless integration
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from core.functions.multi_series_pipeline import MultiSeriesInferencePipeline
|
||||||
|
|
||||||
|
# Multi-series configuration
|
||||||
|
config = MultiSeriesStageConfig(
|
||||||
|
stage_id="detection",
|
||||||
|
multi_series_mode=True,
|
||||||
|
firmware_paths={"KL520": {"scpu": "...", "ncpu": "..."}, ...},
|
||||||
|
model_paths={"KL520": "...", "KL720": "..."}
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import List, Dict, Any, Optional, Callable, Union
|
||||||
|
import threading
|
||||||
|
import queue
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
import numpy as np
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
# Import existing pipeline components
|
||||||
|
from .InferencePipeline import (
|
||||||
|
PipelineData, InferencePipeline, PreProcessor, PostProcessor, DataProcessor
|
||||||
|
)
|
||||||
|
from .Multidongle import MultiDongle
|
||||||
|
|
||||||
|
# Import multi-series manager
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
||||||
|
from multi_series_dongle_manager import MultiSeriesDongleManager
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MultiSeriesStageConfig:
|
||||||
|
"""Enhanced configuration for multi-series pipeline stages"""
|
||||||
|
stage_id: str
|
||||||
|
max_queue_size: int = 100
|
||||||
|
|
||||||
|
# Multi-series mode configuration
|
||||||
|
multi_series_mode: bool = False
|
||||||
|
firmware_paths: Optional[Dict[str, Dict[str, str]]] = None # {"KL520": {"scpu": path, "ncpu": path}}
|
||||||
|
model_paths: Optional[Dict[str, str]] = None # {"KL520": model_path, "KL720": model_path}
|
||||||
|
result_buffer_size: int = 1000
|
||||||
|
|
||||||
|
# Single-series mode configuration (backward compatibility)
|
||||||
|
port_ids: Optional[List[int]] = None
|
||||||
|
scpu_fw_path: Optional[str] = None
|
||||||
|
ncpu_fw_path: Optional[str] = None
|
||||||
|
model_path: Optional[str] = None
|
||||||
|
upload_fw: bool = False
|
||||||
|
|
||||||
|
# Processing configuration
|
||||||
|
input_preprocessor: Optional[PreProcessor] = None
|
||||||
|
output_postprocessor: Optional[PostProcessor] = None
|
||||||
|
stage_preprocessor: Optional[PreProcessor] = None
|
||||||
|
stage_postprocessor: Optional[PostProcessor] = None
|
||||||
|
|
||||||
|
|
||||||
|
class MultiSeriesPipelineStage:
|
||||||
|
"""Enhanced pipeline stage supporting both single and multi-series modes"""
|
||||||
|
|
||||||
|
def __init__(self, config: MultiSeriesStageConfig):
|
||||||
|
self.config = config
|
||||||
|
self.stage_id = config.stage_id
|
||||||
|
|
||||||
|
# Initialize inference engine based on mode
|
||||||
|
if config.multi_series_mode:
|
||||||
|
# Multi-series mode using MultiSeriesDongleManager
|
||||||
|
self.inference_engine = MultiSeriesDongleManager(
|
||||||
|
max_queue_size=config.max_queue_size,
|
||||||
|
result_buffer_size=config.result_buffer_size
|
||||||
|
)
|
||||||
|
self.is_multi_series = True
|
||||||
|
else:
|
||||||
|
# Single-series mode using MultiDongle (backward compatibility)
|
||||||
|
self.inference_engine = MultiDongle(
|
||||||
|
port_id=config.port_ids or [],
|
||||||
|
scpu_fw_path=config.scpu_fw_path or "",
|
||||||
|
ncpu_fw_path=config.ncpu_fw_path or "",
|
||||||
|
model_path=config.model_path or "",
|
||||||
|
upload_fw=config.upload_fw,
|
||||||
|
max_queue_size=config.max_queue_size
|
||||||
|
)
|
||||||
|
self.is_multi_series = False
|
||||||
|
|
||||||
|
# Store processors
|
||||||
|
self.input_preprocessor = config.input_preprocessor
|
||||||
|
self.output_postprocessor = config.output_postprocessor
|
||||||
|
|
||||||
|
# Threading for this stage
|
||||||
|
self.input_queue = queue.Queue(maxsize=config.max_queue_size)
|
||||||
|
self.output_queue = queue.Queue(maxsize=config.max_queue_size)
|
||||||
|
self.worker_thread = None
|
||||||
|
self.running = False
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
|
||||||
|
# Statistics
|
||||||
|
self.processed_count = 0
|
||||||
|
self.error_count = 0
|
||||||
|
self.processing_times = []
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
"""Initialize the stage"""
|
||||||
|
print(f"[Stage {self.stage_id}] Initializing {'multi-series' if self.is_multi_series else 'single-series'} mode...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.is_multi_series:
|
||||||
|
# Initialize multi-series manager
|
||||||
|
if not self.inference_engine.scan_and_initialize_devices(
|
||||||
|
self.config.firmware_paths,
|
||||||
|
self.config.model_paths
|
||||||
|
):
|
||||||
|
raise RuntimeError("Failed to initialize multi-series dongles")
|
||||||
|
print(f"[Stage {self.stage_id}] Multi-series dongles initialized successfully")
|
||||||
|
else:
|
||||||
|
# Initialize single-series MultiDongle
|
||||||
|
self.inference_engine.initialize()
|
||||||
|
print(f"[Stage {self.stage_id}] Single-series dongle initialized successfully")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Stage {self.stage_id}] Initialization failed: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Start the stage worker thread"""
|
||||||
|
if self.worker_thread and self.worker_thread.is_alive():
|
||||||
|
return
|
||||||
|
|
||||||
|
self.running = True
|
||||||
|
self._stop_event.clear()
|
||||||
|
|
||||||
|
# Start inference engine
|
||||||
|
if self.is_multi_series:
|
||||||
|
self.inference_engine.start()
|
||||||
|
else:
|
||||||
|
self.inference_engine.start()
|
||||||
|
|
||||||
|
# Start worker thread
|
||||||
|
self.worker_thread = threading.Thread(target=self._worker_loop, daemon=True)
|
||||||
|
self.worker_thread.start()
|
||||||
|
print(f"[Stage {self.stage_id}] Worker thread started")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop the stage gracefully"""
|
||||||
|
print(f"[Stage {self.stage_id}] Stopping...")
|
||||||
|
self.running = False
|
||||||
|
self._stop_event.set()
|
||||||
|
|
||||||
|
# Put sentinel to unblock worker
|
||||||
|
try:
|
||||||
|
self.input_queue.put(None, timeout=1.0)
|
||||||
|
except queue.Full:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Wait for worker thread
|
||||||
|
if self.worker_thread and self.worker_thread.is_alive():
|
||||||
|
self.worker_thread.join(timeout=3.0)
|
||||||
|
|
||||||
|
# Stop inference engine
|
||||||
|
if self.is_multi_series:
|
||||||
|
self.inference_engine.stop()
|
||||||
|
else:
|
||||||
|
self.inference_engine.stop()
|
||||||
|
print(f"[Stage {self.stage_id}] Stopped")
|
||||||
|
|
||||||
|
def _worker_loop(self):
|
||||||
|
"""Main worker loop for processing data"""
|
||||||
|
print(f"[Stage {self.stage_id}] Worker loop started")
|
||||||
|
|
||||||
|
while self.running and not self._stop_event.is_set():
|
||||||
|
try:
|
||||||
|
# Get input data
|
||||||
|
try:
|
||||||
|
pipeline_data = self.input_queue.get(timeout=1.0)
|
||||||
|
if pipeline_data is None: # Sentinel value
|
||||||
|
continue
|
||||||
|
except queue.Empty:
|
||||||
|
if self._stop_event.is_set():
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
# Process data through this stage
|
||||||
|
processed_data = self._process_data(pipeline_data)
|
||||||
|
|
||||||
|
# Only count and record timing for actual inference results
|
||||||
|
if processed_data and self._has_inference_result(processed_data):
|
||||||
|
processing_time = time.time() - start_time
|
||||||
|
self.processing_times.append(processing_time)
|
||||||
|
if len(self.processing_times) > 1000:
|
||||||
|
self.processing_times = self.processing_times[-500:]
|
||||||
|
|
||||||
|
self.processed_count += 1
|
||||||
|
|
||||||
|
# Put result to output queue
|
||||||
|
try:
|
||||||
|
self.output_queue.put(processed_data, block=False)
|
||||||
|
except queue.Full:
|
||||||
|
# Drop oldest and add new
|
||||||
|
try:
|
||||||
|
self.output_queue.get_nowait()
|
||||||
|
self.output_queue.put(processed_data, block=False)
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.error_count += 1
|
||||||
|
print(f"[Stage {self.stage_id}] Processing error: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
print(f"[Stage {self.stage_id}] Worker loop stopped")
|
||||||
|
|
||||||
|
def _has_inference_result(self, processed_data) -> bool:
|
||||||
|
"""Check if processed_data contains a valid inference result"""
|
||||||
|
if not processed_data:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if hasattr(processed_data, 'stage_results') and processed_data.stage_results:
|
||||||
|
stage_result = processed_data.stage_results.get(self.stage_id)
|
||||||
|
if stage_result:
|
||||||
|
if isinstance(stage_result, tuple) and len(stage_result) == 2:
|
||||||
|
prob, result_str = stage_result
|
||||||
|
return prob is not None and result_str is not None and result_str != 'Processing'
|
||||||
|
elif isinstance(stage_result, dict):
|
||||||
|
if stage_result.get("status") in ["processing", "async"]:
|
||||||
|
return False
|
||||||
|
if stage_result.get("result") == "Processing":
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return stage_result is not None
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _process_data(self, pipeline_data: PipelineData) -> PipelineData:
|
||||||
|
"""Process data through this stage"""
|
||||||
|
try:
|
||||||
|
current_data = pipeline_data.data
|
||||||
|
|
||||||
|
# Step 1: Input preprocessing (inter-stage)
|
||||||
|
if self.input_preprocessor and isinstance(current_data, np.ndarray):
|
||||||
|
if self.is_multi_series:
|
||||||
|
# For multi-series, we may need different preprocessing
|
||||||
|
current_data = self.input_preprocessor.process(current_data, (640, 640), 'BGR565')
|
||||||
|
else:
|
||||||
|
current_data = self.input_preprocessor.process(
|
||||||
|
current_data,
|
||||||
|
self.inference_engine.model_input_shape,
|
||||||
|
'BGR565'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 2: Inference
|
||||||
|
inference_result = None
|
||||||
|
|
||||||
|
if isinstance(current_data, np.ndarray) and len(current_data.shape) == 3:
|
||||||
|
if self.is_multi_series:
|
||||||
|
# Multi-series inference
|
||||||
|
sequence_id = self.inference_engine.put_input(current_data, 'BGR565')
|
||||||
|
|
||||||
|
# Try to get result (non-blocking for async processing)
|
||||||
|
result = self.inference_engine.get_result(timeout=0.1)
|
||||||
|
|
||||||
|
if result is not None:
|
||||||
|
# Extract actual inference data from MultiSeriesDongleManager result
|
||||||
|
if hasattr(result, 'result') and result.result:
|
||||||
|
if isinstance(result.result, tuple) and len(result.result) == 2:
|
||||||
|
inference_result = result.result
|
||||||
|
else:
|
||||||
|
inference_result = result.result
|
||||||
|
else:
|
||||||
|
inference_result = {'probability': 0.0, 'result': 'Processing', 'status': 'async'}
|
||||||
|
else:
|
||||||
|
inference_result = {'probability': 0.0, 'result': 'Processing', 'status': 'async'}
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Single-series inference (existing behavior)
|
||||||
|
processed_data = self.inference_engine.preprocess_frame(current_data, 'BGR565')
|
||||||
|
if processed_data is not None:
|
||||||
|
self.inference_engine.put_input(processed_data, 'BGR565')
|
||||||
|
|
||||||
|
# Get inference result
|
||||||
|
result = self.inference_engine.get_latest_inference_result()
|
||||||
|
|
||||||
|
if result is not None:
|
||||||
|
if isinstance(result, tuple) and len(result) == 2:
|
||||||
|
inference_result = result
|
||||||
|
elif isinstance(result, dict) and result:
|
||||||
|
inference_result = result
|
||||||
|
else:
|
||||||
|
inference_result = result
|
||||||
|
else:
|
||||||
|
inference_result = {'probability': 0.0, 'result': 'Processing', 'status': 'async'}
|
||||||
|
|
||||||
|
# Step 3: Update pipeline data
|
||||||
|
if not inference_result:
|
||||||
|
inference_result = {'probability': 0.0, 'result': 'Processing', 'status': 'async'}
|
||||||
|
|
||||||
|
pipeline_data.stage_results[self.stage_id] = inference_result
|
||||||
|
pipeline_data.data = inference_result
|
||||||
|
pipeline_data.metadata[f'{self.stage_id}_timestamp'] = time.time()
|
||||||
|
|
||||||
|
return pipeline_data
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Stage {self.stage_id}] Data processing error: {e}")
|
||||||
|
pipeline_data.stage_results[self.stage_id] = {
|
||||||
|
'error': str(e),
|
||||||
|
'probability': 0.0,
|
||||||
|
'result': 'Processing Error'
|
||||||
|
}
|
||||||
|
return pipeline_data
|
||||||
|
|
||||||
|
def put_data(self, data: PipelineData, timeout: float = 1.0) -> bool:
|
||||||
|
"""Put data into this stage's input queue"""
|
||||||
|
try:
|
||||||
|
self.input_queue.put(data, timeout=timeout)
|
||||||
|
return True
|
||||||
|
except queue.Full:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_result(self, timeout: float = 0.1) -> Optional[PipelineData]:
|
||||||
|
"""Get result from this stage's output queue"""
|
||||||
|
try:
|
||||||
|
return self.output_queue.get(timeout=timeout)
|
||||||
|
except queue.Empty:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_statistics(self) -> Dict[str, Any]:
|
||||||
|
"""Get stage statistics"""
|
||||||
|
avg_processing_time = (
|
||||||
|
sum(self.processing_times) / len(self.processing_times)
|
||||||
|
if self.processing_times else 0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get engine-specific statistics
|
||||||
|
if self.is_multi_series:
|
||||||
|
engine_stats = self.inference_engine.get_statistics()
|
||||||
|
else:
|
||||||
|
engine_stats = self.inference_engine.get_statistics()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'stage_id': self.stage_id,
|
||||||
|
'mode': 'multi-series' if self.is_multi_series else 'single-series',
|
||||||
|
'processed_count': self.processed_count,
|
||||||
|
'error_count': self.error_count,
|
||||||
|
'avg_processing_time': avg_processing_time,
|
||||||
|
'input_queue_size': self.input_queue.qsize(),
|
||||||
|
'output_queue_size': self.output_queue.qsize(),
|
||||||
|
'engine_stats': engine_stats
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MultiSeriesInferencePipeline(InferencePipeline):
|
||||||
|
"""Enhanced inference pipeline with multi-series support"""
|
||||||
|
|
||||||
|
def __init__(self, stage_configs: List[MultiSeriesStageConfig],
|
||||||
|
final_postprocessor: Optional[PostProcessor] = None,
|
||||||
|
pipeline_name: str = "MultiSeriesInferencePipeline"):
|
||||||
|
"""
|
||||||
|
Initialize multi-series inference pipeline
|
||||||
|
"""
|
||||||
|
self.pipeline_name = pipeline_name
|
||||||
|
self.stage_configs = stage_configs
|
||||||
|
self.final_postprocessor = final_postprocessor
|
||||||
|
|
||||||
|
# Create enhanced stages
|
||||||
|
self.stages: List[MultiSeriesPipelineStage] = []
|
||||||
|
for config in stage_configs:
|
||||||
|
stage = MultiSeriesPipelineStage(config)
|
||||||
|
self.stages.append(stage)
|
||||||
|
|
||||||
|
# Initialize other components from parent class
|
||||||
|
self.coordinator_thread = None
|
||||||
|
self.running = False
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
|
||||||
|
self.pipeline_input_queue = queue.Queue(maxsize=100)
|
||||||
|
self.pipeline_output_queue = queue.Queue(maxsize=100)
|
||||||
|
|
||||||
|
self.result_callback = None
|
||||||
|
self.error_callback = None
|
||||||
|
self.stats_callback = None
|
||||||
|
|
||||||
|
self.pipeline_counter = 0
|
||||||
|
self.completed_counter = 0
|
||||||
|
self.error_counter = 0
|
||||||
|
|
||||||
|
self.fps_start_time = None
|
||||||
|
self.fps_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def create_multi_series_config_from_model_node(model_config: Dict[str, Any]) -> MultiSeriesStageConfig:
|
||||||
|
"""
|
||||||
|
Create MultiSeriesStageConfig from model node configuration
|
||||||
|
"""
|
||||||
|
if model_config.get('multi_series_mode', False):
|
||||||
|
# Multi-series configuration
|
||||||
|
return MultiSeriesStageConfig(
|
||||||
|
stage_id=model_config.get('node_name', 'inference_stage'),
|
||||||
|
multi_series_mode=True,
|
||||||
|
firmware_paths=model_config.get('firmware_paths'),
|
||||||
|
model_paths=model_config.get('model_paths'),
|
||||||
|
max_queue_size=model_config.get('max_queue_size', 100),
|
||||||
|
result_buffer_size=model_config.get('result_buffer_size', 1000)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Single-series configuration (backward compatibility)
|
||||||
|
return MultiSeriesStageConfig(
|
||||||
|
stage_id=model_config.get('node_name', 'inference_stage'),
|
||||||
|
multi_series_mode=False,
|
||||||
|
port_ids=[], # Will be auto-detected
|
||||||
|
scpu_fw_path=model_config.get('scpu_fw_path'),
|
||||||
|
ncpu_fw_path=model_config.get('ncpu_fw_path'),
|
||||||
|
model_path=model_config.get('model_path'),
|
||||||
|
upload_fw=True,
|
||||||
|
max_queue_size=model_config.get('max_queue_size', 50)
|
||||||
|
)
|
||||||
@ -10,11 +10,32 @@ try:
|
|||||||
NODEGRAPH_AVAILABLE = True
|
NODEGRAPH_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
NODEGRAPH_AVAILABLE = False
|
NODEGRAPH_AVAILABLE = False
|
||||||
# Create a mock base class
|
# Create a mock base class with property support
|
||||||
class BaseNode:
|
class BaseNode:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self._properties = {}
|
||||||
|
|
||||||
|
def create_property(self, name, value):
|
||||||
|
self._properties[name] = value
|
||||||
|
|
||||||
|
def set_property(self, name, value):
|
||||||
|
self._properties[name] = value
|
||||||
|
|
||||||
|
def get_property(self, name):
|
||||||
|
return self._properties.get(name, None)
|
||||||
|
|
||||||
|
def add_input(self, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def add_output(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_color(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
return getattr(self, 'NODE_NAME', 'Unknown Node')
|
||||||
|
|
||||||
|
|
||||||
class ExactInputNode(BaseNode):
|
class ExactInputNode(BaseNode):
|
||||||
"""Input data source node - exact match to original."""
|
"""Input data source node - exact match to original."""
|
||||||
@ -73,9 +94,6 @@ class ExactInputNode(BaseNode):
|
|||||||
|
|
||||||
def get_business_properties(self):
|
def get_business_properties(self):
|
||||||
"""Get all business properties for serialization."""
|
"""Get all business properties for serialization."""
|
||||||
if not NODEGRAPH_AVAILABLE:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
properties = {}
|
properties = {}
|
||||||
for prop_name in self._property_options.keys():
|
for prop_name in self._property_options.keys():
|
||||||
try:
|
try:
|
||||||
@ -100,13 +118,21 @@ class ExactModelNode(BaseNode):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
# Setup node connections (NodeGraphQt specific)
|
||||||
if NODEGRAPH_AVAILABLE:
|
if NODEGRAPH_AVAILABLE:
|
||||||
# Setup node connections - exact match
|
|
||||||
self.add_input('input', multi_input=False, color=(255, 140, 0))
|
self.add_input('input', multi_input=False, color=(255, 140, 0))
|
||||||
self.add_output('output', color=(0, 255, 0))
|
self.add_output('output', color=(0, 255, 0))
|
||||||
self.set_color(65, 84, 102)
|
self.set_color(65, 84, 102)
|
||||||
|
|
||||||
# Original properties - exact match
|
# Create properties (always, regardless of NodeGraphQt availability)
|
||||||
|
self.create_property('multi_series_mode', False)
|
||||||
|
|
||||||
|
# Multi-series properties
|
||||||
|
self.create_property('assets_folder', '')
|
||||||
|
self.create_property('enabled_series', ['520', '720'])
|
||||||
|
self.create_property('port_mapping', {})
|
||||||
|
|
||||||
|
# Single-series properties (original)
|
||||||
self.create_property('model_path', '')
|
self.create_property('model_path', '')
|
||||||
self.create_property('scpu_fw_path', '')
|
self.create_property('scpu_fw_path', '')
|
||||||
self.create_property('ncpu_fw_path', '')
|
self.create_property('ncpu_fw_path', '')
|
||||||
@ -115,8 +141,15 @@ class ExactModelNode(BaseNode):
|
|||||||
self.create_property('port_id', '')
|
self.create_property('port_id', '')
|
||||||
self.create_property('upload_fw', True)
|
self.create_property('upload_fw', True)
|
||||||
|
|
||||||
# Original property options - exact match
|
# Property options with multi-series support (always available)
|
||||||
self._property_options = {
|
self._property_options = {
|
||||||
|
# Multi-series properties
|
||||||
|
'multi_series_mode': {'type': 'bool', 'default': False, 'description': 'Enable multi-series dongle support'},
|
||||||
|
'assets_folder': {'type': 'file_path', 'filter': 'Directories', 'mode': 'directory'},
|
||||||
|
'enabled_series': ['520', '720', '630', '730', '540'],
|
||||||
|
'port_mapping': {'type': 'dict', 'description': 'Port ID to series mapping'},
|
||||||
|
|
||||||
|
# Single-series properties (original)
|
||||||
'dongle_series': ['520', '720', '1080', 'Custom'],
|
'dongle_series': ['520', '720', '1080', 'Custom'],
|
||||||
'num_dongles': {'min': 1, 'max': 16},
|
'num_dongles': {'min': 1, 'max': 16},
|
||||||
'model_path': {'type': 'file_path', 'filter': 'NEF Model files (*.nef)'},
|
'model_path': {'type': 'file_path', 'filter': 'NEF Model files (*.nef)'},
|
||||||
@ -126,7 +159,8 @@ class ExactModelNode(BaseNode):
|
|||||||
'upload_fw': {'type': 'bool', 'default': True, 'description': 'Upload firmware to dongle if needed'}
|
'upload_fw': {'type': 'bool', 'default': True, 'description': 'Upload firmware to dongle if needed'}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create custom properties dictionary for UI compatibility
|
# Create custom properties dictionary for UI compatibility (NodeGraphQt specific)
|
||||||
|
if NODEGRAPH_AVAILABLE:
|
||||||
self._populate_custom_properties()
|
self._populate_custom_properties()
|
||||||
|
|
||||||
def _populate_custom_properties(self):
|
def _populate_custom_properties(self):
|
||||||
@ -153,9 +187,6 @@ class ExactModelNode(BaseNode):
|
|||||||
|
|
||||||
def get_business_properties(self):
|
def get_business_properties(self):
|
||||||
"""Get all business properties for serialization."""
|
"""Get all business properties for serialization."""
|
||||||
if not NODEGRAPH_AVAILABLE:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
properties = {}
|
properties = {}
|
||||||
for prop_name in self._property_options.keys():
|
for prop_name in self._property_options.keys():
|
||||||
try:
|
try:
|
||||||
@ -166,8 +197,19 @@ class ExactModelNode(BaseNode):
|
|||||||
|
|
||||||
def get_display_properties(self):
|
def get_display_properties(self):
|
||||||
"""Return properties that should be displayed in the UI panel."""
|
"""Return properties that should be displayed in the UI panel."""
|
||||||
# Customize which properties appear for Model nodes
|
# Check if multi-series mode is enabled
|
||||||
return ['model_path', 'scpu_fw_path', 'ncpu_fw_path', 'dongle_series', 'num_dongles', 'port_id', 'upload_fw']
|
multi_series_enabled = False
|
||||||
|
try:
|
||||||
|
multi_series_enabled = self.get_property('multi_series_mode')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if multi_series_enabled:
|
||||||
|
# Multi-series mode properties
|
||||||
|
return ['multi_series_mode', 'assets_folder', 'enabled_series', 'port_mapping']
|
||||||
|
else:
|
||||||
|
# Single-series mode properties (original)
|
||||||
|
return ['multi_series_mode', 'model_path', 'scpu_fw_path', 'ncpu_fw_path', 'dongle_series', 'num_dongles', 'port_id', 'upload_fw']
|
||||||
|
|
||||||
|
|
||||||
class ExactPreprocessNode(BaseNode):
|
class ExactPreprocessNode(BaseNode):
|
||||||
@ -226,9 +268,6 @@ class ExactPreprocessNode(BaseNode):
|
|||||||
|
|
||||||
def get_business_properties(self):
|
def get_business_properties(self):
|
||||||
"""Get all business properties for serialization."""
|
"""Get all business properties for serialization."""
|
||||||
if not NODEGRAPH_AVAILABLE:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
properties = {}
|
properties = {}
|
||||||
for prop_name in self._property_options.keys():
|
for prop_name in self._property_options.keys():
|
||||||
try:
|
try:
|
||||||
@ -294,9 +333,6 @@ class ExactPostprocessNode(BaseNode):
|
|||||||
|
|
||||||
def get_business_properties(self):
|
def get_business_properties(self):
|
||||||
"""Get all business properties for serialization."""
|
"""Get all business properties for serialization."""
|
||||||
if not NODEGRAPH_AVAILABLE:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
properties = {}
|
properties = {}
|
||||||
for prop_name in self._property_options.keys():
|
for prop_name in self._property_options.keys():
|
||||||
try:
|
try:
|
||||||
@ -361,9 +397,6 @@ class ExactOutputNode(BaseNode):
|
|||||||
|
|
||||||
def get_business_properties(self):
|
def get_business_properties(self):
|
||||||
"""Get all business properties for serialization."""
|
"""Get all business properties for serialization."""
|
||||||
if not NODEGRAPH_AVAILABLE:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
properties = {}
|
properties = {}
|
||||||
for prop_name in self._property_options.keys():
|
for prop_name in self._property_options.keys():
|
||||||
try:
|
try:
|
||||||
|
|||||||
121
main.py
121
main.py
@ -49,41 +49,95 @@ class SingleInstance:
|
|||||||
self.lock_file = None
|
self.lock_file = None
|
||||||
self.lock_fd = None
|
self.lock_fd = None
|
||||||
|
|
||||||
|
def _cleanup_stale_lock(self):
|
||||||
|
"""Clean up stale lock files from previous crashes."""
|
||||||
|
try:
|
||||||
|
lock_path = os.path.join(tempfile.gettempdir(), f"{self.app_name}.lock")
|
||||||
|
if os.path.exists(lock_path):
|
||||||
|
# Try to remove stale lock file
|
||||||
|
if HAS_FCNTL:
|
||||||
|
# On Unix systems, try to acquire lock to check if process is still alive
|
||||||
|
try:
|
||||||
|
test_fd = os.open(lock_path, os.O_RDWR)
|
||||||
|
fcntl.lockf(test_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
# If we got the lock, previous process is dead
|
||||||
|
os.close(test_fd)
|
||||||
|
os.unlink(lock_path)
|
||||||
|
except (OSError, IOError):
|
||||||
|
# Lock is held by another process
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# On Windows, just try to remove the file
|
||||||
|
# If it's locked by another process, this will fail
|
||||||
|
try:
|
||||||
|
os.unlink(lock_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def is_running(self):
|
def is_running(self):
|
||||||
"""Check if another instance is already running."""
|
"""Check if another instance is already running."""
|
||||||
# Try to create shared memory
|
# First, clean up any stale locks
|
||||||
if self.shared_memory.attach():
|
self._cleanup_stale_lock()
|
||||||
# Another instance is already running
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
# Try to attach to existing shared memory
|
||||||
|
if self.shared_memory.attach():
|
||||||
|
# Try to write to shared memory to verify it's valid
|
||||||
|
try:
|
||||||
|
# If we can attach but can't access, it might be stale
|
||||||
|
self.shared_memory.detach()
|
||||||
|
# Try to create new shared memory
|
||||||
|
if self.shared_memory.create(1):
|
||||||
|
# Successfully created, no other instance
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# Failed to create, another instance exists
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
# Shared memory is stale, try to create new one
|
||||||
|
if not self.shared_memory.create(1):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
# Try to create the shared memory
|
# Try to create the shared memory
|
||||||
if not self.shared_memory.create(1):
|
if not self.shared_memory.create(1):
|
||||||
# Failed to create, likely another instance exists
|
# Failed to create, likely another instance exists
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Also use file locking as backup (works better on some systems)
|
# Also use file locking as backup
|
||||||
|
try:
|
||||||
|
self.lock_file = os.path.join(tempfile.gettempdir(), f"{self.app_name}.lock")
|
||||||
if HAS_FCNTL:
|
if HAS_FCNTL:
|
||||||
try:
|
self.lock_fd = os.open(self.lock_file, os.O_CREAT | os.O_WRONLY, 0o644)
|
||||||
self.lock_file = os.path.join(tempfile.gettempdir(), f"{self.app_name}.lock")
|
|
||||||
self.lock_fd = os.open(self.lock_file, os.O_CREAT | os.O_EXCL | os.O_RDWR)
|
|
||||||
fcntl.lockf(self.lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
fcntl.lockf(self.lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
except (OSError, IOError):
|
# Write PID to lock file
|
||||||
# Another instance is running
|
os.write(self.lock_fd, str(os.getpid()).encode())
|
||||||
if self.lock_fd:
|
os.fsync(self.lock_fd)
|
||||||
os.close(self.lock_fd)
|
|
||||||
return True
|
|
||||||
else:
|
else:
|
||||||
# On Windows, try simple file creation
|
# On Windows, use exclusive create
|
||||||
try:
|
|
||||||
self.lock_file = os.path.join(tempfile.gettempdir(), f"{self.app_name}.lock")
|
|
||||||
self.lock_fd = os.open(self.lock_file, os.O_CREAT | os.O_EXCL | os.O_RDWR)
|
self.lock_fd = os.open(self.lock_file, os.O_CREAT | os.O_EXCL | os.O_RDWR)
|
||||||
|
os.write(self.lock_fd, str(os.getpid()).encode())
|
||||||
except (OSError, IOError):
|
except (OSError, IOError):
|
||||||
|
# Another instance is running or we can't create lock
|
||||||
|
self._cleanup_on_error()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _cleanup_on_error(self):
|
||||||
|
"""Clean up resources when instance check fails."""
|
||||||
|
try:
|
||||||
|
if self.shared_memory.isAttached():
|
||||||
|
self.shared_memory.detach()
|
||||||
|
if self.lock_fd:
|
||||||
|
os.close(self.lock_fd)
|
||||||
|
self.lock_fd = None
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
"""Clean up resources."""
|
"""Clean up resources."""
|
||||||
|
try:
|
||||||
if self.shared_memory.isAttached():
|
if self.shared_memory.isAttached():
|
||||||
self.shared_memory.detach()
|
self.shared_memory.detach()
|
||||||
|
|
||||||
@ -92,8 +146,13 @@ class SingleInstance:
|
|||||||
if HAS_FCNTL:
|
if HAS_FCNTL:
|
||||||
fcntl.lockf(self.lock_fd, fcntl.LOCK_UN)
|
fcntl.lockf(self.lock_fd, fcntl.LOCK_UN)
|
||||||
os.close(self.lock_fd)
|
os.close(self.lock_fd)
|
||||||
|
if self.lock_file and os.path.exists(self.lock_file):
|
||||||
os.unlink(self.lock_file)
|
os.unlink(self.lock_file)
|
||||||
except:
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
self.lock_fd = None
|
||||||
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@ -125,6 +184,9 @@ def setup_application():
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main application entry point."""
|
"""Main application entry point."""
|
||||||
|
single_instance = None
|
||||||
|
|
||||||
|
try:
|
||||||
# Create a minimal QApplication first for the message box
|
# Create a minimal QApplication first for the message box
|
||||||
temp_app = QApplication(sys.argv) if not QApplication.instance() else QApplication.instance()
|
temp_app = QApplication(sys.argv) if not QApplication.instance() else QApplication.instance()
|
||||||
|
|
||||||
@ -137,9 +199,9 @@ def main():
|
|||||||
"Application Already Running",
|
"Application Already Running",
|
||||||
"Cluster4NPU is already running. Please check your taskbar or system tray.",
|
"Cluster4NPU is already running. Please check your taskbar or system tray.",
|
||||||
)
|
)
|
||||||
|
single_instance.cleanup()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
try:
|
|
||||||
# Setup the full application
|
# Setup the full application
|
||||||
app = setup_application()
|
app = setup_application()
|
||||||
|
|
||||||
@ -147,18 +209,37 @@ def main():
|
|||||||
dashboard = DashboardLogin()
|
dashboard = DashboardLogin()
|
||||||
dashboard.show()
|
dashboard.show()
|
||||||
|
|
||||||
# Clean up single instance on app exit
|
# Set up cleanup handlers
|
||||||
app.aboutToQuit.connect(single_instance.cleanup)
|
app.aboutToQuit.connect(single_instance.cleanup)
|
||||||
|
|
||||||
|
# Also handle system signals for cleanup
|
||||||
|
import signal
|
||||||
|
def signal_handler(signum, frame):
|
||||||
|
print(f"Received signal {signum}, cleaning up...")
|
||||||
|
single_instance.cleanup()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
# Start the application event loop
|
# Start the application event loop
|
||||||
sys.exit(app.exec_())
|
exit_code = app.exec_()
|
||||||
|
|
||||||
|
# Ensure cleanup even if aboutToQuit wasn't called
|
||||||
|
single_instance.cleanup()
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error starting application: {e}")
|
print(f"Error starting application: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
if single_instance:
|
||||||
single_instance.cleanup()
|
single_instance.cleanup()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
finally:
|
||||||
|
# Final cleanup attempt
|
||||||
|
if single_instance:
|
||||||
|
single_instance.cleanup()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
347
test_multi_series_integration.py
Normal file
347
test_multi_series_integration.py
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
"""
|
||||||
|
Test Multi-Series Dongle Integration
|
||||||
|
|
||||||
|
This test script validates the complete multi-series dongle integration
|
||||||
|
including the enhanced model node, converter, and pipeline components.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python test_multi_series_integration.py
|
||||||
|
|
||||||
|
This will create a test assets folder structure and validate all components.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add project root to path
|
||||||
|
project_root = Path(__file__).parent
|
||||||
|
sys.path.insert(0, str(project_root))
|
||||||
|
|
||||||
|
def test_exact_model_node():
|
||||||
|
"""Test the enhanced ExactModelNode functionality"""
|
||||||
|
print("🧪 Testing ExactModelNode...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from core.nodes.exact_nodes import ExactModelNode, NODEGRAPH_AVAILABLE
|
||||||
|
|
||||||
|
if not NODEGRAPH_AVAILABLE:
|
||||||
|
print("⚠️ NodeGraphQt not available, testing limited functionality")
|
||||||
|
# Test basic instantiation
|
||||||
|
node = ExactModelNode()
|
||||||
|
print("✅ ExactModelNode basic instantiation works")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Create node and test properties
|
||||||
|
node = ExactModelNode()
|
||||||
|
|
||||||
|
# Test single-series mode (default)
|
||||||
|
assert node.get_property('multi_series_mode') == False
|
||||||
|
assert node.get_property('dongle_series') == '520'
|
||||||
|
assert node.get_property('max_queue_size') == 100
|
||||||
|
|
||||||
|
# Test property display logic
|
||||||
|
display_props = node.get_display_properties()
|
||||||
|
expected_single_series = [
|
||||||
|
'multi_series_mode', 'model_path', 'scpu_fw_path', 'ncpu_fw_path',
|
||||||
|
'dongle_series', 'num_dongles', 'port_id', 'upload_fw'
|
||||||
|
]
|
||||||
|
assert display_props == expected_single_series
|
||||||
|
|
||||||
|
# Test multi-series mode
|
||||||
|
node.set_property('multi_series_mode', True)
|
||||||
|
display_props = node.get_display_properties()
|
||||||
|
expected_multi_series = [
|
||||||
|
'multi_series_mode', 'assets_folder', 'enabled_series',
|
||||||
|
'max_queue_size', 'result_buffer_size', 'batch_size',
|
||||||
|
'enable_preprocessing', 'enable_postprocessing'
|
||||||
|
]
|
||||||
|
assert display_props == expected_multi_series
|
||||||
|
|
||||||
|
# Test inference config generation
|
||||||
|
config = node.get_inference_config()
|
||||||
|
assert config['multi_series_mode'] == True
|
||||||
|
assert 'enabled_series' in config
|
||||||
|
|
||||||
|
# Test hardware requirements
|
||||||
|
hw_req = node.get_hardware_requirements()
|
||||||
|
assert hw_req['multi_series_mode'] == True
|
||||||
|
|
||||||
|
print("✅ ExactModelNode functionality tests passed")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ ExactModelNode test failed: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_multi_series_setup_utility():
|
||||||
|
"""Test the multi-series setup utility"""
|
||||||
|
print("🧪 Testing multi-series setup utility...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from utils.multi_series_setup import MultiSeriesSetup
|
||||||
|
|
||||||
|
# Create temporary directory for testing
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
# Test folder structure creation
|
||||||
|
success = MultiSeriesSetup.create_folder_structure(temp_dir, ['520', '720'])
|
||||||
|
assert success, "Failed to create folder structure"
|
||||||
|
|
||||||
|
assets_path = os.path.join(temp_dir, 'Assets')
|
||||||
|
assert os.path.exists(assets_path), "Assets folder not created"
|
||||||
|
|
||||||
|
# Check structure
|
||||||
|
firmware_path = os.path.join(assets_path, 'Firmware')
|
||||||
|
models_path = os.path.join(assets_path, 'Models')
|
||||||
|
assert os.path.exists(firmware_path), "Firmware folder not created"
|
||||||
|
assert os.path.exists(models_path), "Models folder not created"
|
||||||
|
|
||||||
|
# Check series folders
|
||||||
|
for series in ['520', '720']:
|
||||||
|
series_fw = os.path.join(firmware_path, f'KL{series}')
|
||||||
|
series_model = os.path.join(models_path, f'KL{series}')
|
||||||
|
assert os.path.exists(series_fw), f"KL{series} firmware folder not created"
|
||||||
|
assert os.path.exists(series_model), f"KL{series} models folder not created"
|
||||||
|
|
||||||
|
# Test validation (should fail initially - no files)
|
||||||
|
is_valid, issues = MultiSeriesSetup.validate_folder_structure(assets_path)
|
||||||
|
assert not is_valid, "Validation should fail with empty folders"
|
||||||
|
assert len(issues) > 0, "Should have validation issues"
|
||||||
|
|
||||||
|
# Test series listing
|
||||||
|
series_info = MultiSeriesSetup.list_available_series(assets_path)
|
||||||
|
assert len(series_info) == 0, "Should have no valid series initially"
|
||||||
|
|
||||||
|
print("✅ Multi-series setup utility tests passed")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Multi-series setup utility test failed: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_multi_series_converter():
|
||||||
|
"""Test the multi-series MFlow converter"""
|
||||||
|
print("🧪 Testing multi-series converter...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from core.functions.multi_series_mflow_converter import MultiSeriesMFlowConverter
|
||||||
|
|
||||||
|
# Create test mflow data
|
||||||
|
test_mflow_data = {
|
||||||
|
"project_name": "Test Multi-Series Pipeline",
|
||||||
|
"description": "Test pipeline with multi-series configuration",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "input_1",
|
||||||
|
"name": "Input Node",
|
||||||
|
"type": "input_node",
|
||||||
|
"custom": {
|
||||||
|
"source_type": "Camera",
|
||||||
|
"resolution": "640x480"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "model_1",
|
||||||
|
"name": "Multi-Series Model",
|
||||||
|
"type": "model_node",
|
||||||
|
"custom": {
|
||||||
|
"multi_series_mode": True,
|
||||||
|
"assets_folder": "/test/assets",
|
||||||
|
"enabled_series": ["520", "720"],
|
||||||
|
"max_queue_size": 100,
|
||||||
|
"result_buffer_size": 1000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "output_1",
|
||||||
|
"name": "Output Node",
|
||||||
|
"type": "output_node",
|
||||||
|
"custom": {
|
||||||
|
"output_type": "Display"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": [
|
||||||
|
{"input_node": "input_1", "output_node": "model_1"},
|
||||||
|
{"input_node": "model_1", "output_node": "output_1"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test converter instantiation
|
||||||
|
converter = MultiSeriesMFlowConverter()
|
||||||
|
|
||||||
|
# Test basic conversion (will fail validation due to missing files, but should parse)
|
||||||
|
try:
|
||||||
|
config = converter._convert_mflow_to_enhanced_config(test_mflow_data)
|
||||||
|
|
||||||
|
# Check basic structure
|
||||||
|
assert config.pipeline_name == "Test Multi-Series Pipeline"
|
||||||
|
assert len(config.stage_configs) > 0
|
||||||
|
assert config.has_multi_series == True
|
||||||
|
assert config.multi_series_count == 1
|
||||||
|
|
||||||
|
print("✅ Multi-series converter basic parsing works")
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
# Expected to fail validation due to missing assets folder
|
||||||
|
if "not found" in str(e):
|
||||||
|
print("✅ Multi-series converter correctly validates missing assets")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
print("✅ Multi-series converter tests passed")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Multi-series converter test failed: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_pipeline_components():
|
||||||
|
"""Test multi-series pipeline components"""
|
||||||
|
print("🧪 Testing pipeline components...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from core.functions.multi_series_pipeline import (
|
||||||
|
MultiSeriesStageConfig,
|
||||||
|
MultiSeriesPipelineStage,
|
||||||
|
create_multi_series_config_from_model_node
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test MultiSeriesStageConfig creation
|
||||||
|
config = MultiSeriesStageConfig(
|
||||||
|
stage_id="test_stage",
|
||||||
|
multi_series_mode=True,
|
||||||
|
firmware_paths={"KL520": {"scpu": "test.bin", "ncpu": "test.bin"}},
|
||||||
|
model_paths={"KL520": "test.nef"},
|
||||||
|
max_queue_size=100
|
||||||
|
)
|
||||||
|
|
||||||
|
assert config.stage_id == "test_stage"
|
||||||
|
assert config.multi_series_mode == True
|
||||||
|
assert config.max_queue_size == 100
|
||||||
|
|
||||||
|
# Test config creation from model node
|
||||||
|
model_config = {
|
||||||
|
'multi_series_mode': True,
|
||||||
|
'node_name': 'test_node',
|
||||||
|
'firmware_paths': {"KL520": {"scpu": "test.bin", "ncpu": "test.bin"}},
|
||||||
|
'model_paths': {"KL520": "test.nef"},
|
||||||
|
'max_queue_size': 50
|
||||||
|
}
|
||||||
|
|
||||||
|
stage_config = create_multi_series_config_from_model_node(model_config)
|
||||||
|
assert stage_config.multi_series_mode == True
|
||||||
|
assert stage_config.stage_id == 'test_node'
|
||||||
|
|
||||||
|
print("✅ Pipeline components tests passed")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Pipeline components test failed: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def create_test_assets_structure():
|
||||||
|
"""Create a complete test assets structure for manual testing"""
|
||||||
|
print("🏗️ Creating test assets structure...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from utils.multi_series_setup import MultiSeriesSetup
|
||||||
|
|
||||||
|
# Create test structure in project directory
|
||||||
|
test_assets_path = os.path.join(project_root, "test_assets")
|
||||||
|
|
||||||
|
if os.path.exists(test_assets_path):
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(test_assets_path)
|
||||||
|
|
||||||
|
# Create structure
|
||||||
|
success = MultiSeriesSetup.create_folder_structure(
|
||||||
|
project_root,
|
||||||
|
series_list=['520', '720', '730']
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
assets_full_path = os.path.join(project_root, "Assets")
|
||||||
|
print(f"✅ Test assets structure created at: {assets_full_path}")
|
||||||
|
print("\n📋 To complete the setup:")
|
||||||
|
print("1. Copy your firmware files to Assets/Firmware/KLxxx/ folders")
|
||||||
|
print("2. Copy your model files to Assets/Models/KLxxx/ folders")
|
||||||
|
print("3. Run validation: python -m utils.multi_series_setup validate --path Assets")
|
||||||
|
print("4. Configure your model node to use the Assets folder")
|
||||||
|
|
||||||
|
return assets_full_path
|
||||||
|
else:
|
||||||
|
print("❌ Failed to create test assets structure")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error creating test assets structure: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def run_all_tests():
|
||||||
|
"""Run all integration tests"""
|
||||||
|
print("🚀 Starting Multi-Series Dongle Integration Tests\n")
|
||||||
|
|
||||||
|
tests = [
|
||||||
|
("ExactModelNode", test_exact_model_node),
|
||||||
|
("Setup Utility", test_multi_series_setup_utility),
|
||||||
|
("Converter", test_multi_series_converter),
|
||||||
|
("Pipeline Components", test_pipeline_components)
|
||||||
|
]
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
for test_name, test_func in tests:
|
||||||
|
print(f"\n{'='*50}")
|
||||||
|
print(f"Testing: {test_name}")
|
||||||
|
print(f"{'='*50}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = test_func()
|
||||||
|
results[test_name] = result
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ {test_name} test crashed: {e}")
|
||||||
|
results[test_name] = False
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
print(f"\n{'='*50}")
|
||||||
|
print("📊 TEST SUMMARY")
|
||||||
|
print(f"{'='*50}")
|
||||||
|
|
||||||
|
passed = sum(1 for r in results.values() if r)
|
||||||
|
total = len(results)
|
||||||
|
|
||||||
|
for test_name, result in results.items():
|
||||||
|
status = "✅ PASS" if result else "❌ FAIL"
|
||||||
|
print(f"{test_name:<20} {status}")
|
||||||
|
|
||||||
|
print(f"\nResults: {passed}/{total} tests passed")
|
||||||
|
|
||||||
|
if passed == total:
|
||||||
|
print("🎉 All tests passed! Multi-series integration is ready.")
|
||||||
|
|
||||||
|
# Offer to create test structure
|
||||||
|
response = input("\n❓ Create test assets structure for manual testing? (y/n): ")
|
||||||
|
if response.lower() in ['y', 'yes']:
|
||||||
|
create_test_assets_structure()
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("⚠️ Some tests failed. Check the output above for details.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = run_all_tests()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
167
test_ui_folder_selection.py
Normal file
167
test_ui_folder_selection.py
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
"""
|
||||||
|
Test UI Folder Selection
|
||||||
|
|
||||||
|
Simple test to verify that the folder selection UI works correctly
|
||||||
|
for the assets_folder property in multi-series mode.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python test_ui_folder_selection.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add project root to path
|
||||||
|
project_root = Path(__file__).parent
|
||||||
|
sys.path.insert(0, str(project_root))
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
PYQT_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
PYQT_AVAILABLE = False
|
||||||
|
|
||||||
|
def test_folder_selection_ui():
|
||||||
|
"""Test the folder selection UI components"""
|
||||||
|
|
||||||
|
if not PYQT_AVAILABLE:
|
||||||
|
print("❌ PyQt5 not available, cannot test UI components")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
from core.nodes.exact_nodes import ExactModelNode, NODEGRAPH_AVAILABLE
|
||||||
|
|
||||||
|
if not NODEGRAPH_AVAILABLE:
|
||||||
|
print("❌ NodeGraphQt not available, cannot test node properties UI")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Create QApplication
|
||||||
|
app = QApplication(sys.argv) if not QApplication.instance() else QApplication.instance()
|
||||||
|
|
||||||
|
# Create test node
|
||||||
|
node = ExactModelNode()
|
||||||
|
|
||||||
|
# Enable multi-series mode
|
||||||
|
node.set_property('multi_series_mode', True)
|
||||||
|
|
||||||
|
# Test property access
|
||||||
|
assets_folder = node.get_property('assets_folder')
|
||||||
|
enabled_series = node.get_property('enabled_series')
|
||||||
|
|
||||||
|
print(f"✅ Node created successfully")
|
||||||
|
print(f" - assets_folder: '{assets_folder}'")
|
||||||
|
print(f" - enabled_series: {enabled_series}")
|
||||||
|
print(f" - multi_series_mode: {node.get_property('multi_series_mode')}")
|
||||||
|
|
||||||
|
# Get property options
|
||||||
|
property_options = node._property_options
|
||||||
|
assets_folder_options = property_options.get('assets_folder', {})
|
||||||
|
enabled_series_options = property_options.get('enabled_series', {})
|
||||||
|
|
||||||
|
print(f"✅ Property options configured correctly")
|
||||||
|
print(f" - assets_folder type: {assets_folder_options.get('type')}")
|
||||||
|
print(f" - enabled_series type: {enabled_series_options.get('type')}")
|
||||||
|
print(f" - enabled_series options: {enabled_series_options.get('options')}")
|
||||||
|
|
||||||
|
# Test display properties
|
||||||
|
display_props = node.get_display_properties()
|
||||||
|
print(f"✅ Display properties for multi-series mode: {display_props}")
|
||||||
|
|
||||||
|
# Verify multi-series specific properties are included
|
||||||
|
expected_props = ['assets_folder', 'enabled_series']
|
||||||
|
missing_props = [prop for prop in expected_props if prop not in display_props]
|
||||||
|
|
||||||
|
if missing_props:
|
||||||
|
print(f"❌ Missing properties in display: {missing_props}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"✅ All multi-series properties present in UI")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Test failed: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def create_test_assets_folder():
|
||||||
|
"""Create a test assets folder for UI testing"""
|
||||||
|
try:
|
||||||
|
from utils.multi_series_setup import MultiSeriesSetup
|
||||||
|
|
||||||
|
test_path = os.path.join(project_root, "test_ui_assets")
|
||||||
|
|
||||||
|
# Remove existing test folder
|
||||||
|
if os.path.exists(test_path):
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(test_path)
|
||||||
|
|
||||||
|
# Create new test structure
|
||||||
|
success = MultiSeriesSetup.create_folder_structure(
|
||||||
|
project_root.parent, # Create in parent directory to avoid clutter
|
||||||
|
series_list=['520', '720']
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
assets_path = os.path.join(project_root.parent, "Assets")
|
||||||
|
print(f"✅ Test assets folder created: {assets_path}")
|
||||||
|
print("📋 You can now:")
|
||||||
|
print("1. Run your UI application")
|
||||||
|
print("2. Create a Model Node")
|
||||||
|
print("3. Enable 'Multi-Series Mode'")
|
||||||
|
print("4. Use 'Browse Folder' button for 'Assets Folder'")
|
||||||
|
print(f"5. Select the folder: {assets_path}")
|
||||||
|
return assets_path
|
||||||
|
else:
|
||||||
|
print("❌ Failed to create test assets folder")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error creating test assets: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main test function"""
|
||||||
|
print("🧪 Testing UI Folder Selection for Multi-Series Configuration\n")
|
||||||
|
|
||||||
|
# Test 1: Node property configuration
|
||||||
|
print("=" * 50)
|
||||||
|
print("Test 1: Node Property Configuration")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
success = test_folder_selection_ui()
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
print("❌ UI component test failed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Test 2: Create test assets folder
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("Test 2: Create Test Assets Folder")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
assets_path = create_test_assets_folder()
|
||||||
|
|
||||||
|
if assets_path:
|
||||||
|
print("\n🎉 UI folder selection test completed successfully!")
|
||||||
|
print("\n📋 Manual Testing Steps:")
|
||||||
|
print("1. Run: python main.py")
|
||||||
|
print("2. Create a new pipeline")
|
||||||
|
print("3. Add a Model Node")
|
||||||
|
print("4. In properties panel, enable 'Multi-Series Mode'")
|
||||||
|
print("5. Click 'Browse Folder' for 'Assets Folder'")
|
||||||
|
print(f"6. Select folder: {assets_path}")
|
||||||
|
print("7. Configure 'Enabled Series' checkboxes")
|
||||||
|
print("8. Save and deploy pipeline")
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("❌ Test assets creation failed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = main()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
@ -38,6 +38,22 @@ from PyQt5.QtGui import QFont, QColor, QPalette, QImage, QPixmap
|
|||||||
# Import our converter and pipeline system
|
# Import our converter and pipeline system
|
||||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'core', 'functions'))
|
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'core', 'functions'))
|
||||||
|
|
||||||
|
# Multi-series imports
|
||||||
|
try:
|
||||||
|
from ui.dialogs.multi_series_config import MultiSeriesConfigDialog
|
||||||
|
MULTI_SERIES_UI_AVAILABLE = True
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"Warning: Multi-series UI not available: {e}")
|
||||||
|
MULTI_SERIES_UI_AVAILABLE = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
from multi_series_dongle_manager import MultiSeriesDongleManager
|
||||||
|
from core.functions.multi_series_mflow_converter import MultiSeriesMFlowConverter
|
||||||
|
MULTI_SERIES_BACKEND_AVAILABLE = True
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"Warning: Multi-series backend not available: {e}")
|
||||||
|
MULTI_SERIES_BACKEND_AVAILABLE = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from core.functions.mflow_converter import MFlowConverter, PipelineConfig
|
from core.functions.mflow_converter import MFlowConverter, PipelineConfig
|
||||||
CONVERTER_AVAILABLE = True
|
CONVERTER_AVAILABLE = True
|
||||||
@ -119,15 +135,112 @@ class DeploymentWorker(QThread):
|
|||||||
result_updated = pyqtSignal(dict) # For inference results
|
result_updated = pyqtSignal(dict) # For inference results
|
||||||
terminal_output = pyqtSignal(str) # For terminal output in GUI
|
terminal_output = pyqtSignal(str) # For terminal output in GUI
|
||||||
stdout_captured = pyqtSignal(str) # For captured stdout/stderr
|
stdout_captured = pyqtSignal(str) # For captured stdout/stderr
|
||||||
|
multi_series_status = pyqtSignal(dict) # For multi-series dongle status
|
||||||
|
|
||||||
def __init__(self, pipeline_data: Dict[str, Any]):
|
def __init__(self, pipeline_data: Dict[str, Any], multi_series_config: Dict[str, Any] = None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.pipeline_data = pipeline_data
|
self.pipeline_data = pipeline_data
|
||||||
|
self.multi_series_config = multi_series_config
|
||||||
self.should_stop = False
|
self.should_stop = False
|
||||||
self.orchestrator = None
|
self.orchestrator = None
|
||||||
|
self.multi_series_manager = None
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Main deployment workflow."""
|
"""Main deployment workflow."""
|
||||||
|
try:
|
||||||
|
# Check if this is a multi-series deployment
|
||||||
|
is_multi_series = self._check_multi_series_mode()
|
||||||
|
|
||||||
|
if is_multi_series and self.multi_series_config:
|
||||||
|
self._run_multi_series_deployment()
|
||||||
|
else:
|
||||||
|
self._run_single_series_deployment()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.error_occurred.emit(f"Deployment error: {str(e)}")
|
||||||
|
|
||||||
|
def _check_multi_series_mode(self) -> bool:
|
||||||
|
"""Check if any nodes are configured for multi-series mode"""
|
||||||
|
nodes = self.pipeline_data.get('nodes', [])
|
||||||
|
for node in nodes:
|
||||||
|
# Check for any Model node type (including ExactModelNode)
|
||||||
|
if 'Model' in node.get('type', ''):
|
||||||
|
# Check properties in order of preference
|
||||||
|
node_properties = node.get('properties', {}) # New format
|
||||||
|
if not node_properties:
|
||||||
|
node_properties = node.get('custom_properties', {}) # Fallback 1
|
||||||
|
if not node_properties:
|
||||||
|
node_properties = node.get('custom', {}) # Fallback 2
|
||||||
|
|
||||||
|
if node_properties.get('multi_series_mode', False):
|
||||||
|
print(f"Multi-series mode detected in node: {node.get('name', 'Unknown')}")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _run_multi_series_deployment(self):
|
||||||
|
"""Run multi-series deployment workflow"""
|
||||||
|
try:
|
||||||
|
# Step 1: Convert to multi-series configuration
|
||||||
|
self.progress_updated.emit(10, "Converting to multi-series configuration...")
|
||||||
|
|
||||||
|
if not MULTI_SERIES_BACKEND_AVAILABLE:
|
||||||
|
self.error_occurred.emit("Multi-series backend not available. Please check installation.")
|
||||||
|
return
|
||||||
|
|
||||||
|
converter = MultiSeriesMFlowConverter()
|
||||||
|
multi_series_config = converter.convert_to_multi_series(
|
||||||
|
self.pipeline_data,
|
||||||
|
self.multi_series_config
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 2: Validate multi-series configuration
|
||||||
|
self.progress_updated.emit(30, "Validating multi-series configuration...")
|
||||||
|
is_valid, issues = converter.validate_multi_series_config(multi_series_config)
|
||||||
|
|
||||||
|
if not is_valid:
|
||||||
|
error_msg = "Multi-series configuration validation failed:\n" + "\n".join(issues)
|
||||||
|
self.error_occurred.emit(error_msg)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.progress_updated.emit(50, "Configuration validation passed")
|
||||||
|
|
||||||
|
# Step 3: Initialize MultiSeriesDongleManager
|
||||||
|
self.progress_updated.emit(60, "Initializing multi-series dongle manager...")
|
||||||
|
|
||||||
|
self.multi_series_manager = converter.create_multi_series_manager(multi_series_config)
|
||||||
|
if not self.multi_series_manager:
|
||||||
|
self.error_occurred.emit("Failed to initialize multi-series dongle manager")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.progress_updated.emit(80, "Starting multi-series inference...")
|
||||||
|
self.deployment_started.emit()
|
||||||
|
|
||||||
|
# Start the multi-series manager
|
||||||
|
self.multi_series_manager.start()
|
||||||
|
|
||||||
|
# Emit status
|
||||||
|
status_info = {
|
||||||
|
'type': 'multi_series',
|
||||||
|
'enabled_series': multi_series_config.enabled_series,
|
||||||
|
'total_gops': sum([
|
||||||
|
{'KL520': 3, 'KL720': 28, 'KL630': 400, 'KL730': 1600, 'KL540': 800}.get(series, 0)
|
||||||
|
for series in multi_series_config.enabled_series
|
||||||
|
]),
|
||||||
|
'port_mapping': multi_series_config.port_mapping
|
||||||
|
}
|
||||||
|
self.multi_series_status.emit(status_info)
|
||||||
|
|
||||||
|
self.progress_updated.emit(100, "Multi-series pipeline deployed successfully!")
|
||||||
|
self.deployment_completed.emit(True, f"Multi-series pipeline deployed with {len(multi_series_config.enabled_series)} series")
|
||||||
|
|
||||||
|
# Keep running and processing results
|
||||||
|
self._process_multi_series_results()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.error_occurred.emit(f"Multi-series deployment failed: {str(e)}")
|
||||||
|
|
||||||
|
def _run_single_series_deployment(self):
|
||||||
|
"""Run single-series deployment workflow (original behavior)"""
|
||||||
try:
|
try:
|
||||||
# Step 1: Convert .mflow to pipeline config
|
# Step 1: Convert .mflow to pipeline config
|
||||||
self.progress_updated.emit(10, "Converting pipeline configuration...")
|
self.progress_updated.emit(10, "Converting pipeline configuration...")
|
||||||
@ -236,11 +349,56 @@ class DeploymentWorker(QThread):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.error_occurred.emit(f"Deployment error: {str(e)}")
|
self.error_occurred.emit(f"Deployment error: {str(e)}")
|
||||||
|
|
||||||
|
def _process_multi_series_results(self):
|
||||||
|
"""Process results from multi-series manager"""
|
||||||
|
import cv2
|
||||||
|
|
||||||
|
try:
|
||||||
|
while not self.should_stop:
|
||||||
|
# Get result from multi-series manager
|
||||||
|
result = self.multi_series_manager.get_result(timeout=0.1)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
# Process result for UI display
|
||||||
|
result_dict = {
|
||||||
|
'sequence_id': result.sequence_id,
|
||||||
|
'dongle_series': result.dongle_series,
|
||||||
|
'timestamp': result.timestamp,
|
||||||
|
'stage_results': {
|
||||||
|
f'{result.dongle_series}_stage': result.result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Emit result for GUI display
|
||||||
|
self.result_updated.emit(result_dict)
|
||||||
|
|
||||||
|
# Emit terminal output
|
||||||
|
terminal_text = f"[{result.dongle_series}] Sequence {result.sequence_id}: Processed"
|
||||||
|
self.terminal_output.emit(terminal_text)
|
||||||
|
|
||||||
|
# Get and emit statistics
|
||||||
|
stats = self.multi_series_manager.get_statistics()
|
||||||
|
status_info = {
|
||||||
|
'type': 'multi_series',
|
||||||
|
'stats': stats,
|
||||||
|
'current_loads': stats.get('current_loads', {}),
|
||||||
|
'total_processed': stats.get('total_completed', 0),
|
||||||
|
'queue_size': stats.get('input_queue_size', 0)
|
||||||
|
}
|
||||||
|
self.multi_series_status.emit(status_info)
|
||||||
|
|
||||||
|
self.msleep(10) # Small delay to prevent busy waiting
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.error_occurred.emit(f"Error processing multi-series results: {str(e)}")
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Stop the deployment process."""
|
"""Stop the deployment process."""
|
||||||
self.should_stop = True
|
self.should_stop = True
|
||||||
if self.orchestrator:
|
if self.orchestrator:
|
||||||
self.orchestrator.stop()
|
self.orchestrator.stop()
|
||||||
|
if self.multi_series_manager:
|
||||||
|
self.multi_series_manager.stop()
|
||||||
|
|
||||||
def _format_terminal_results(self, result_dict):
|
def _format_terminal_results(self, result_dict):
|
||||||
"""Format inference results for terminal display in GUI."""
|
"""Format inference results for terminal display in GUI."""
|
||||||
@ -341,12 +499,75 @@ class DeploymentDialog(QDialog):
|
|||||||
self.pipeline_data = pipeline_data
|
self.pipeline_data = pipeline_data
|
||||||
self.deployment_worker = None
|
self.deployment_worker = None
|
||||||
self.pipeline_config = None
|
self.pipeline_config = None
|
||||||
|
self.is_multi_series = self._check_multi_series_nodes()
|
||||||
|
|
||||||
self.setWindowTitle("Deploy Pipeline to Dongles")
|
# Extract multi-series configuration if needed
|
||||||
|
if self.is_multi_series:
|
||||||
|
self.multi_series_config = self._extract_multi_series_config()
|
||||||
|
else:
|
||||||
|
self.multi_series_config = None
|
||||||
|
|
||||||
|
title = "Deploy Multi-Series Pipeline" if self.is_multi_series else "Deploy Pipeline to Dongles"
|
||||||
|
self.setWindowTitle(title)
|
||||||
self.setMinimumSize(800, 600)
|
self.setMinimumSize(800, 600)
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
self.apply_theme()
|
self.apply_theme()
|
||||||
|
|
||||||
|
def _check_multi_series_nodes(self) -> bool:
|
||||||
|
"""Check if pipeline has multi-series enabled nodes"""
|
||||||
|
nodes = self.pipeline_data.get('nodes', [])
|
||||||
|
for node in nodes:
|
||||||
|
# Check for any Model node type (including ExactModelNode)
|
||||||
|
if 'Model' in node.get('type', ''):
|
||||||
|
# Check properties in order of preference
|
||||||
|
node_properties = node.get('properties', {}) # New format
|
||||||
|
if not node_properties:
|
||||||
|
node_properties = node.get('custom_properties', {}) # Fallback 1
|
||||||
|
if not node_properties:
|
||||||
|
node_properties = node.get('custom', {}) # Fallback 2
|
||||||
|
|
||||||
|
if node_properties.get('multi_series_mode', False):
|
||||||
|
print(f"Multi-series node detected: {node.get('name', 'Unknown')}")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _extract_multi_series_config(self):
|
||||||
|
"""Extract multi-series configuration from node properties"""
|
||||||
|
multi_series_config = {
|
||||||
|
'language': 'en',
|
||||||
|
'enabled_series': [],
|
||||||
|
'config_mode': 'folder',
|
||||||
|
'assets_folder': '',
|
||||||
|
'port_mapping': {},
|
||||||
|
'individual_paths': {}
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes = self.pipeline_data.get('nodes', [])
|
||||||
|
for node in nodes:
|
||||||
|
if 'Model' in node.get('type', ''):
|
||||||
|
# Check properties in order of preference
|
||||||
|
node_properties = node.get('properties', {})
|
||||||
|
if not node_properties:
|
||||||
|
node_properties = node.get('custom_properties', {})
|
||||||
|
if not node_properties:
|
||||||
|
node_properties = node.get('custom', {})
|
||||||
|
|
||||||
|
if node_properties.get('multi_series_mode', False):
|
||||||
|
# Extract multi-series configuration
|
||||||
|
multi_series_config['enabled_series'] = node_properties.get('enabled_series', [])
|
||||||
|
multi_series_config['assets_folder'] = node_properties.get('assets_folder', '')
|
||||||
|
multi_series_config['port_mapping'] = node_properties.get('port_mapping', {})
|
||||||
|
|
||||||
|
# Determine config mode based on assets_folder
|
||||||
|
if multi_series_config['assets_folder']:
|
||||||
|
multi_series_config['config_mode'] = 'folder'
|
||||||
|
else:
|
||||||
|
multi_series_config['config_mode'] = 'individual'
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
return multi_series_config
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
"""Setup the dialog UI."""
|
"""Setup the dialog UI."""
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
@ -395,11 +616,18 @@ class DeploymentDialog(QDialog):
|
|||||||
# Buttons
|
# Buttons
|
||||||
button_layout = QHBoxLayout()
|
button_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
# Multi-series configuration button (only for multi-series pipelines)
|
||||||
|
if self.is_multi_series:
|
||||||
|
self.configure_multi_series_btn = QPushButton("Configure Multi-Series")
|
||||||
|
self.configure_multi_series_btn.clicked.connect(self.configure_multi_series)
|
||||||
|
button_layout.addWidget(self.configure_multi_series_btn)
|
||||||
|
|
||||||
self.analyze_button = QPushButton("Analyze Pipeline")
|
self.analyze_button = QPushButton("Analyze Pipeline")
|
||||||
self.analyze_button.clicked.connect(self.analyze_pipeline)
|
self.analyze_button.clicked.connect(self.analyze_pipeline)
|
||||||
button_layout.addWidget(self.analyze_button)
|
button_layout.addWidget(self.analyze_button)
|
||||||
|
|
||||||
self.deploy_button = QPushButton("Deploy to Dongles")
|
deploy_text = "Deploy Multi-Series Pipeline" if self.is_multi_series else "Deploy to Dongles"
|
||||||
|
self.deploy_button = QPushButton(deploy_text)
|
||||||
self.deploy_button.clicked.connect(self.start_deployment)
|
self.deploy_button.clicked.connect(self.start_deployment)
|
||||||
self.deploy_button.setEnabled(False)
|
self.deploy_button.setEnabled(False)
|
||||||
button_layout.addWidget(self.deploy_button)
|
button_layout.addWidget(self.deploy_button)
|
||||||
@ -421,6 +649,31 @@ class DeploymentDialog(QDialog):
|
|||||||
# Populate initial data
|
# Populate initial data
|
||||||
self.populate_overview()
|
self.populate_overview()
|
||||||
|
|
||||||
|
def configure_multi_series(self):
|
||||||
|
"""Open multi-series configuration dialog"""
|
||||||
|
if not MULTI_SERIES_UI_AVAILABLE:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"Configuration Error",
|
||||||
|
"Multi-series configuration UI not available. Please check installation."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create and show multi-series configuration dialog
|
||||||
|
config_dialog = MultiSeriesConfigDialog(self, self.multi_series_config)
|
||||||
|
|
||||||
|
if config_dialog.exec_() == config_dialog.Accepted:
|
||||||
|
self.multi_series_config = config_dialog.get_configuration()
|
||||||
|
|
||||||
|
# Update status
|
||||||
|
enabled_series = self.multi_series_config.get('enabled_series', [])
|
||||||
|
if enabled_series:
|
||||||
|
self.dongle_status.setText(f"Multi-series configured: {', '.join(enabled_series)}")
|
||||||
|
self.deploy_button.setEnabled(True)
|
||||||
|
else:
|
||||||
|
self.dongle_status.setText("No series configured")
|
||||||
|
self.deploy_button.setEnabled(False)
|
||||||
|
|
||||||
def create_overview_tab(self) -> QWidget:
|
def create_overview_tab(self) -> QWidget:
|
||||||
"""Create pipeline overview tab."""
|
"""Create pipeline overview tab."""
|
||||||
widget = QWidget()
|
widget = QWidget()
|
||||||
@ -534,7 +787,27 @@ class DeploymentDialog(QDialog):
|
|||||||
|
|
||||||
layout.addWidget(splitter)
|
layout.addWidget(splitter)
|
||||||
|
|
||||||
# Dongle status (placeholder)
|
# Dongle status
|
||||||
|
if self.is_multi_series:
|
||||||
|
status_group = QGroupBox("Multi-Series Dongle Status")
|
||||||
|
status_layout = QVBoxLayout(status_group)
|
||||||
|
|
||||||
|
# Multi-series status table
|
||||||
|
self.multi_series_status_table = QTableWidget()
|
||||||
|
self.multi_series_status_table.setColumnCount(4)
|
||||||
|
self.multi_series_status_table.setHorizontalHeaderLabels([
|
||||||
|
"Series", "Port IDs", "Current Load", "Total Processed"
|
||||||
|
])
|
||||||
|
self.multi_series_status_table.horizontalHeader().setStretchLastSection(True)
|
||||||
|
self.multi_series_status_table.setMaximumHeight(150)
|
||||||
|
status_layout.addWidget(self.multi_series_status_table)
|
||||||
|
|
||||||
|
# Overall status
|
||||||
|
self.dongle_status = QLabel("Configure multi-series settings to begin")
|
||||||
|
self.dongle_status.setAlignment(Qt.AlignCenter)
|
||||||
|
status_layout.addWidget(self.dongle_status)
|
||||||
|
|
||||||
|
else:
|
||||||
status_group = QGroupBox("Dongle Status")
|
status_group = QGroupBox("Dongle Status")
|
||||||
status_layout = QVBoxLayout(status_group)
|
status_layout = QVBoxLayout(status_group)
|
||||||
|
|
||||||
@ -705,11 +978,17 @@ Stage Configurations:
|
|||||||
|
|
||||||
def start_deployment(self):
|
def start_deployment(self):
|
||||||
"""Start the deployment process."""
|
"""Start the deployment process."""
|
||||||
if not self.pipeline_config:
|
if not self.pipeline_config and not self.is_multi_series:
|
||||||
QMessageBox.warning(self, "Deployment Error",
|
QMessageBox.warning(self, "Deployment Error",
|
||||||
"Please analyze the pipeline first.")
|
"Please analyze the pipeline first.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# For multi-series pipelines, check if configuration is done
|
||||||
|
if self.is_multi_series and not self.multi_series_config:
|
||||||
|
QMessageBox.warning(self, "Configuration Required",
|
||||||
|
"Please configure multi-series settings first.")
|
||||||
|
return
|
||||||
|
|
||||||
# Switch to deployment tab
|
# Switch to deployment tab
|
||||||
self.tab_widget.setCurrentIndex(3)
|
self.tab_widget.setCurrentIndex(3)
|
||||||
|
|
||||||
@ -726,7 +1005,7 @@ Stage Configurations:
|
|||||||
self.terminal_output_display.append("Pipeline deployment started - terminal output will appear here...")
|
self.terminal_output_display.append("Pipeline deployment started - terminal output will appear here...")
|
||||||
|
|
||||||
# Create and start deployment worker
|
# Create and start deployment worker
|
||||||
self.deployment_worker = DeploymentWorker(self.pipeline_data)
|
self.deployment_worker = DeploymentWorker(self.pipeline_data, self.multi_series_config)
|
||||||
self.deployment_worker.progress_updated.connect(self.update_progress)
|
self.deployment_worker.progress_updated.connect(self.update_progress)
|
||||||
self.deployment_worker.topology_analyzed.connect(self.update_topology_results)
|
self.deployment_worker.topology_analyzed.connect(self.update_topology_results)
|
||||||
self.deployment_worker.conversion_completed.connect(self.on_conversion_completed)
|
self.deployment_worker.conversion_completed.connect(self.on_conversion_completed)
|
||||||
@ -737,6 +1016,7 @@ Stage Configurations:
|
|||||||
self.deployment_worker.result_updated.connect(self.update_inference_results)
|
self.deployment_worker.result_updated.connect(self.update_inference_results)
|
||||||
self.deployment_worker.terminal_output.connect(self.update_terminal_output)
|
self.deployment_worker.terminal_output.connect(self.update_terminal_output)
|
||||||
self.deployment_worker.stdout_captured.connect(self.update_terminal_output)
|
self.deployment_worker.stdout_captured.connect(self.update_terminal_output)
|
||||||
|
self.deployment_worker.multi_series_status.connect(self.update_multi_series_status)
|
||||||
|
|
||||||
|
|
||||||
self.deployment_worker.start()
|
self.deployment_worker.start()
|
||||||
@ -917,6 +1197,71 @@ Stage Configurations:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error updating terminal output: {e}")
|
print(f"Error updating terminal output: {e}")
|
||||||
|
|
||||||
|
def update_multi_series_status(self, status_info: dict):
|
||||||
|
"""Update multi-series dongle status display"""
|
||||||
|
try:
|
||||||
|
if not self.is_multi_series:
|
||||||
|
return
|
||||||
|
|
||||||
|
status_type = status_info.get('type', '')
|
||||||
|
|
||||||
|
if status_type == 'multi_series':
|
||||||
|
# Update overall status
|
||||||
|
enabled_series = status_info.get('enabled_series', [])
|
||||||
|
total_gops = status_info.get('total_gops', 0)
|
||||||
|
|
||||||
|
if enabled_series:
|
||||||
|
status_text = f"Running: {', '.join(enabled_series)} ({total_gops} total GOPS)"
|
||||||
|
self.dongle_status.setText(status_text)
|
||||||
|
|
||||||
|
# Update status table
|
||||||
|
stats = status_info.get('stats', {})
|
||||||
|
current_loads = status_info.get('current_loads', {})
|
||||||
|
port_mapping = status_info.get('port_mapping', {})
|
||||||
|
|
||||||
|
if hasattr(self, 'multi_series_status_table'):
|
||||||
|
# Group port IDs by series
|
||||||
|
series_ports = {}
|
||||||
|
for port_id, series in port_mapping.items():
|
||||||
|
if series not in series_ports:
|
||||||
|
series_ports[series] = []
|
||||||
|
series_ports[series].append(str(port_id))
|
||||||
|
|
||||||
|
# Update table
|
||||||
|
self.multi_series_status_table.setRowCount(len(enabled_series))
|
||||||
|
|
||||||
|
for i, series in enumerate(enabled_series):
|
||||||
|
# Series name
|
||||||
|
self.multi_series_status_table.setItem(i, 0, QTableWidgetItem(series))
|
||||||
|
|
||||||
|
# Port IDs
|
||||||
|
ports = series_ports.get(series, [])
|
||||||
|
ports_text = ", ".join(ports) if ports else "Not mapped"
|
||||||
|
self.multi_series_status_table.setItem(i, 1, QTableWidgetItem(ports_text))
|
||||||
|
|
||||||
|
# Current load
|
||||||
|
# Find product_id for this series
|
||||||
|
product_id = None
|
||||||
|
series_specs = {'KL520': 0x100, 'KL720': 0x720, 'KL630': 0x630, 'KL730': 0x730, 'KL540': 0x540}
|
||||||
|
product_id = series_specs.get(series)
|
||||||
|
|
||||||
|
if product_id and product_id in current_loads:
|
||||||
|
load = current_loads[product_id]
|
||||||
|
self.multi_series_status_table.setItem(i, 2, QTableWidgetItem(str(load)))
|
||||||
|
else:
|
||||||
|
self.multi_series_status_table.setItem(i, 2, QTableWidgetItem("0"))
|
||||||
|
|
||||||
|
# Total processed
|
||||||
|
dongle_stats = stats.get('dongle_stats', {})
|
||||||
|
if product_id and product_id in dongle_stats:
|
||||||
|
processed = dongle_stats[product_id].get('received', 0)
|
||||||
|
self.multi_series_status_table.setItem(i, 3, QTableWidgetItem(str(processed)))
|
||||||
|
else:
|
||||||
|
self.multi_series_status_table.setItem(i, 3, QTableWidgetItem("0"))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating multi-series status: {e}")
|
||||||
|
|
||||||
def apply_theme(self):
|
def apply_theme(self):
|
||||||
"""Apply consistent theme to the dialog."""
|
"""Apply consistent theme to the dialog."""
|
||||||
self.setStyleSheet("""
|
self.setStyleSheet("""
|
||||||
|
|||||||
1207
ui/dialogs/multi_series_config.py
Normal file
1207
ui/dialogs/multi_series_config.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -1140,10 +1140,16 @@ class IntegratedPipelineDashboard(QMainWindow):
|
|||||||
# Get node properties - try different methods
|
# Get node properties - try different methods
|
||||||
try:
|
try:
|
||||||
properties = {}
|
properties = {}
|
||||||
|
# Initialize variables that might be used later in form layout
|
||||||
|
node_type = node.__class__.__name__
|
||||||
|
multi_series_enabled = False
|
||||||
|
|
||||||
# Method 1: Try custom properties (for enhanced nodes)
|
# Method 1: Try custom properties (for enhanced nodes)
|
||||||
if hasattr(node, 'get_business_properties'):
|
if hasattr(node, 'get_business_properties'):
|
||||||
properties = node.get_business_properties()
|
properties = node.get_business_properties()
|
||||||
|
# For Model nodes, check if multi-series is enabled
|
||||||
|
if 'Model' in node_type and hasattr(node, 'get_property'):
|
||||||
|
multi_series_enabled = node.get_property('multi_series_mode') if hasattr(node, 'get_property') else False
|
||||||
|
|
||||||
# Method 1.5: Try ExactNode properties (with _property_options)
|
# Method 1.5: Try ExactNode properties (with _property_options)
|
||||||
elif hasattr(node, '_property_options') and node._property_options:
|
elif hasattr(node, '_property_options') and node._property_options:
|
||||||
@ -1155,6 +1161,9 @@ class IntegratedPipelineDashboard(QMainWindow):
|
|||||||
except:
|
except:
|
||||||
# If property doesn't exist, use a default value
|
# If property doesn't exist, use a default value
|
||||||
properties[prop_name] = None
|
properties[prop_name] = None
|
||||||
|
# For Model nodes, check if multi-series is enabled
|
||||||
|
if 'Model' in node_type and hasattr(node, 'get_property'):
|
||||||
|
multi_series_enabled = node.get_property('multi_series_mode') if hasattr(node, 'get_property') else False
|
||||||
|
|
||||||
# Method 2: Try standard NodeGraphQt properties
|
# Method 2: Try standard NodeGraphQt properties
|
||||||
elif hasattr(node, 'properties'):
|
elif hasattr(node, 'properties'):
|
||||||
@ -1163,10 +1172,15 @@ class IntegratedPipelineDashboard(QMainWindow):
|
|||||||
for key, value in all_props.items():
|
for key, value in all_props.items():
|
||||||
if not key.startswith('_') and key not in ['name', 'selected', 'disabled', 'custom']:
|
if not key.startswith('_') and key not in ['name', 'selected', 'disabled', 'custom']:
|
||||||
properties[key] = value
|
properties[key] = value
|
||||||
|
# For Model nodes, check if multi-series is enabled
|
||||||
|
if 'Model' in node_type:
|
||||||
|
multi_series_enabled = properties.get('multi_series_mode', False)
|
||||||
|
|
||||||
# Method 3: Use exact original properties based on node type
|
# Method 3: Use exact original properties based on node type
|
||||||
else:
|
else:
|
||||||
node_type = node.__class__.__name__
|
# Variables already initialized above
|
||||||
|
properties = {} # Initialize properties dict
|
||||||
|
|
||||||
if 'Input' in node_type:
|
if 'Input' in node_type:
|
||||||
# Exact InputNode properties from original
|
# Exact InputNode properties from original
|
||||||
properties = {
|
properties = {
|
||||||
@ -1177,8 +1191,23 @@ class IntegratedPipelineDashboard(QMainWindow):
|
|||||||
'fps': node.get_property('fps') if hasattr(node, 'get_property') else 30
|
'fps': node.get_property('fps') if hasattr(node, 'get_property') else 30
|
||||||
}
|
}
|
||||||
elif 'Model' in node_type:
|
elif 'Model' in node_type:
|
||||||
# Exact ModelNode properties from original - including upload_fw checkbox
|
# Check if multi-series mode is enabled
|
||||||
|
multi_series_enabled = node.get_property('multi_series_mode') if hasattr(node, 'get_property') else False
|
||||||
|
|
||||||
|
# Basic properties always shown
|
||||||
properties = {
|
properties = {
|
||||||
|
'multi_series_mode': multi_series_enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if multi_series_enabled:
|
||||||
|
# Multi-series mode properties
|
||||||
|
properties.update({
|
||||||
|
'assets_folder': node.get_property('assets_folder') if hasattr(node, 'get_property') else '',
|
||||||
|
'enabled_series': node.get_property('enabled_series') if hasattr(node, 'get_property') else ['520', '720']
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# Single-series mode properties (original)
|
||||||
|
properties.update({
|
||||||
'model_path': node.get_property('model_path') if hasattr(node, 'get_property') else '',
|
'model_path': node.get_property('model_path') if hasattr(node, 'get_property') else '',
|
||||||
'scpu_fw_path': node.get_property('scpu_fw_path') if hasattr(node, 'get_property') else '',
|
'scpu_fw_path': node.get_property('scpu_fw_path') if hasattr(node, 'get_property') else '',
|
||||||
'ncpu_fw_path': node.get_property('ncpu_fw_path') if hasattr(node, 'get_property') else '',
|
'ncpu_fw_path': node.get_property('ncpu_fw_path') if hasattr(node, 'get_property') else '',
|
||||||
@ -1186,7 +1215,7 @@ class IntegratedPipelineDashboard(QMainWindow):
|
|||||||
'num_dongles': node.get_property('num_dongles') if hasattr(node, 'get_property') else 1,
|
'num_dongles': node.get_property('num_dongles') if hasattr(node, 'get_property') else 1,
|
||||||
'port_id': node.get_property('port_id') if hasattr(node, 'get_property') else '',
|
'port_id': node.get_property('port_id') if hasattr(node, 'get_property') else '',
|
||||||
'upload_fw': node.get_property('upload_fw') if hasattr(node, 'get_property') else True
|
'upload_fw': node.get_property('upload_fw') if hasattr(node, 'get_property') else True
|
||||||
}
|
})
|
||||||
elif 'Preprocess' in node_type:
|
elif 'Preprocess' in node_type:
|
||||||
# Exact PreprocessNode properties from original
|
# Exact PreprocessNode properties from original
|
||||||
properties = {
|
properties = {
|
||||||
@ -1219,9 +1248,30 @@ class IntegratedPipelineDashboard(QMainWindow):
|
|||||||
widget = self.create_property_widget_enhanced(node, prop_name, prop_value)
|
widget = self.create_property_widget_enhanced(node, prop_name, prop_value)
|
||||||
|
|
||||||
# Add to form with appropriate labels
|
# Add to form with appropriate labels
|
||||||
if prop_name == 'upload_fw':
|
if prop_name in ['upload_fw', 'multi_series_mode']:
|
||||||
# For upload_fw, don't show a separate label since the checkbox has its own text
|
# For checkboxes with their own text, don't show a separate label
|
||||||
form_layout.addRow(widget)
|
form_layout.addRow(widget)
|
||||||
|
elif prop_name == 'assets_folder':
|
||||||
|
form_layout.addRow("Assets Folder:", widget)
|
||||||
|
elif prop_name == 'enabled_series':
|
||||||
|
form_layout.addRow("Enabled Series:", widget)
|
||||||
|
|
||||||
|
# Add port mapping widget for multi-series mode
|
||||||
|
if 'Model' in node_type and multi_series_enabled:
|
||||||
|
port_mapping_widget = self.create_port_mapping_widget(node)
|
||||||
|
form_layout.addRow(port_mapping_widget)
|
||||||
|
elif prop_name == 'dongle_series':
|
||||||
|
form_layout.addRow("Dongle Series:", widget)
|
||||||
|
elif prop_name == 'num_dongles':
|
||||||
|
form_layout.addRow("Number of Dongles:", widget)
|
||||||
|
elif prop_name == 'port_id':
|
||||||
|
form_layout.addRow("Port ID:", widget)
|
||||||
|
elif prop_name == 'model_path':
|
||||||
|
form_layout.addRow("Model Path:", widget)
|
||||||
|
elif prop_name == 'scpu_fw_path':
|
||||||
|
form_layout.addRow("SCPU Firmware:", widget)
|
||||||
|
elif prop_name == 'ncpu_fw_path':
|
||||||
|
form_layout.addRow("NCPU Firmware:", widget)
|
||||||
else:
|
else:
|
||||||
label = prop_name.replace('_', ' ').title()
|
label = prop_name.replace('_', ' ').title()
|
||||||
form_layout.addRow(f"{label}:", widget)
|
form_layout.addRow(f"{label}:", widget)
|
||||||
@ -1325,7 +1375,7 @@ class IntegratedPipelineDashboard(QMainWindow):
|
|||||||
|
|
||||||
# Check for file path properties first (from prop_options or name pattern)
|
# Check for file path properties first (from prop_options or name pattern)
|
||||||
if (prop_options and isinstance(prop_options, dict) and prop_options.get('type') == 'file_path') or \
|
if (prop_options and isinstance(prop_options, dict) and prop_options.get('type') == 'file_path') or \
|
||||||
prop_name in ['model_path', 'source_path', 'destination']:
|
prop_name in ['model_path', 'source_path', 'destination', 'assets_folder']:
|
||||||
# File path property with smart truncation and width limits
|
# File path property with smart truncation and width limits
|
||||||
display_text = self.truncate_path_smart(str(prop_value)) if prop_value else 'Select File...'
|
display_text = self.truncate_path_smart(str(prop_value)) if prop_value else 'Select File...'
|
||||||
widget = QPushButton(display_text)
|
widget = QPushButton(display_text)
|
||||||
@ -1357,6 +1407,23 @@ class IntegratedPipelineDashboard(QMainWindow):
|
|||||||
widget.setToolTip(f"Full path: {full_path}\n\nClick to browse for {prop_name.replace('_', ' ')}")
|
widget.setToolTip(f"Full path: {full_path}\n\nClick to browse for {prop_name.replace('_', ' ')}")
|
||||||
|
|
||||||
def browse_file():
|
def browse_file():
|
||||||
|
# Handle assets_folder as folder dialog
|
||||||
|
if prop_name == 'assets_folder':
|
||||||
|
folder_path = QFileDialog.getExistingDirectory(
|
||||||
|
self,
|
||||||
|
'Select Multi-Series Assets Folder',
|
||||||
|
str(prop_value) if prop_value else os.path.expanduser("~")
|
||||||
|
)
|
||||||
|
if folder_path:
|
||||||
|
# Update button text with truncated path
|
||||||
|
truncated_text = self.truncate_path_smart(folder_path)
|
||||||
|
widget.setText(truncated_text)
|
||||||
|
# Update tooltip with full path
|
||||||
|
widget.setToolTip(f"Assets Folder: {folder_path}\n\nContains Firmware/ and Models/ subdirectories")
|
||||||
|
# Set property with full path
|
||||||
|
if hasattr(node, 'set_property'):
|
||||||
|
node.set_property(prop_name, folder_path)
|
||||||
|
else:
|
||||||
# Use filter from prop_options if available, otherwise use defaults
|
# Use filter from prop_options if available, otherwise use defaults
|
||||||
if prop_options and 'filter' in prop_options:
|
if prop_options and 'filter' in prop_options:
|
||||||
file_filter = prop_options['filter']
|
file_filter = prop_options['filter']
|
||||||
@ -1384,6 +1451,63 @@ class IntegratedPipelineDashboard(QMainWindow):
|
|||||||
|
|
||||||
widget.clicked.connect(browse_file)
|
widget.clicked.connect(browse_file)
|
||||||
|
|
||||||
|
# Check for enabled_series (special multi-select property)
|
||||||
|
elif prop_name == 'enabled_series':
|
||||||
|
# Create a custom widget for multi-series selection
|
||||||
|
widget = QWidget()
|
||||||
|
layout = QVBoxLayout(widget)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(2)
|
||||||
|
|
||||||
|
# Available series options
|
||||||
|
available_series = ['KL520', 'KL720', 'KL630', 'KL730', 'KL540']
|
||||||
|
current_selection = prop_value if isinstance(prop_value, list) else [prop_value] if prop_value else []
|
||||||
|
|
||||||
|
# Convert to series names if they're just numbers
|
||||||
|
if current_selection and all(isinstance(x, str) and x.isdigit() for x in current_selection):
|
||||||
|
current_selection = [f'KL{x}' for x in current_selection]
|
||||||
|
|
||||||
|
checkboxes = []
|
||||||
|
for series in available_series:
|
||||||
|
checkbox = QCheckBox(f"{series}")
|
||||||
|
checkbox.setChecked(series in current_selection)
|
||||||
|
checkbox.setStyleSheet("""
|
||||||
|
QCheckBox {
|
||||||
|
color: #cdd6f4;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
QCheckBox::indicator {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid #45475a;
|
||||||
|
background-color: #313244;
|
||||||
|
}
|
||||||
|
QCheckBox::indicator:checked {
|
||||||
|
background-color: #a6e3a1;
|
||||||
|
border-color: #a6e3a1;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
layout.addWidget(checkbox)
|
||||||
|
checkboxes.append((series, checkbox))
|
||||||
|
|
||||||
|
# Update function for checkboxes
|
||||||
|
def update_enabled_series():
|
||||||
|
selected = []
|
||||||
|
for series, checkbox in checkboxes:
|
||||||
|
if checkbox.isChecked():
|
||||||
|
# Store just the number for compatibility
|
||||||
|
series_number = series.replace('KL', '')
|
||||||
|
selected.append(series_number)
|
||||||
|
|
||||||
|
if hasattr(node, 'set_property'):
|
||||||
|
node.set_property(prop_name, selected)
|
||||||
|
|
||||||
|
# Connect all checkboxes to update function
|
||||||
|
for _, checkbox in checkboxes:
|
||||||
|
checkbox.toggled.connect(update_enabled_series)
|
||||||
|
|
||||||
# Check for dropdown properties (list options from prop_options or predefined)
|
# Check for dropdown properties (list options from prop_options or predefined)
|
||||||
elif (prop_options and isinstance(prop_options, list)) or \
|
elif (prop_options and isinstance(prop_options, list)) or \
|
||||||
prop_name in ['source_type', 'dongle_series', 'output_format', 'format', 'output_type', 'resolution']:
|
prop_name in ['source_type', 'dongle_series', 'output_format', 'format', 'output_type', 'resolution']:
|
||||||
@ -1455,7 +1579,7 @@ class IntegratedPipelineDashboard(QMainWindow):
|
|||||||
widget = QCheckBox()
|
widget = QCheckBox()
|
||||||
widget.setChecked(prop_value)
|
widget.setChecked(prop_value)
|
||||||
|
|
||||||
# Add special styling for upload_fw checkbox
|
# Add special styling and text for specific checkboxes
|
||||||
if prop_name == 'upload_fw':
|
if prop_name == 'upload_fw':
|
||||||
widget.setText("Upload Firmware to Device")
|
widget.setText("Upload Firmware to Device")
|
||||||
widget.setStyleSheet("""
|
widget.setStyleSheet("""
|
||||||
@ -1479,6 +1603,31 @@ class IntegratedPipelineDashboard(QMainWindow):
|
|||||||
border-color: #74c7ec;
|
border-color: #74c7ec;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
elif prop_name == 'multi_series_mode':
|
||||||
|
widget.setText("Enable Multi-Series Mode")
|
||||||
|
widget.setStyleSheet("""
|
||||||
|
QCheckBox {
|
||||||
|
color: #f9e2af;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
QCheckBox::indicator {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid #f9e2af;
|
||||||
|
background-color: #313244;
|
||||||
|
}
|
||||||
|
QCheckBox::indicator:checked {
|
||||||
|
background-color: #a6e3a1;
|
||||||
|
border-color: #a6e3a1;
|
||||||
|
}
|
||||||
|
QCheckBox::indicator:hover {
|
||||||
|
border-color: #f38ba8;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
widget.setToolTip("Enable multi-series mode to use different dongle models simultaneously")
|
||||||
else:
|
else:
|
||||||
widget.setStyleSheet("""
|
widget.setStyleSheet("""
|
||||||
QCheckBox {
|
QCheckBox {
|
||||||
@ -1506,6 +1655,12 @@ class IntegratedPipelineDashboard(QMainWindow):
|
|||||||
if prop_name == 'upload_fw':
|
if prop_name == 'upload_fw':
|
||||||
status = "enabled" if state == 2 else "disabled"
|
status = "enabled" if state == 2 else "disabled"
|
||||||
print(f"Upload Firmware {status} for {node.name()}")
|
print(f"Upload Firmware {status} for {node.name()}")
|
||||||
|
# For multi_series_mode, refresh the properties panel
|
||||||
|
elif prop_name == 'multi_series_mode':
|
||||||
|
status = "enabled" if state == 2 else "disabled"
|
||||||
|
print(f"Multi-series mode {status} for {node.name()}")
|
||||||
|
# Trigger properties panel refresh to show/hide multi-series properties
|
||||||
|
self.update_node_properties_panel(node)
|
||||||
|
|
||||||
widget.stateChanged.connect(on_change)
|
widget.stateChanged.connect(on_change)
|
||||||
|
|
||||||
@ -1711,42 +1866,152 @@ class IntegratedPipelineDashboard(QMainWindow):
|
|||||||
|
|
||||||
|
|
||||||
def detect_dongles(self):
|
def detect_dongles(self):
|
||||||
"""Detect available dongles using actual device scanning."""
|
"""Enhanced dongle detection supporting both single and multi-series configurations."""
|
||||||
if not self.dongles_list:
|
if not self.dongles_list:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.dongles_list.clear()
|
self.dongles_list.clear()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Import MultiDongle for device scanning
|
# Import both scanning methods
|
||||||
from core.functions.Multidongle import MultiDongle
|
from core.functions.Multidongle import MultiDongle
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
# Scan for available devices
|
# Add path for multi-series manager
|
||||||
|
current_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
sys.path.insert(0, current_dir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from multi_series_dongle_manager import MultiSeriesDongleManager, DongleSeriesSpec
|
||||||
|
multi_series_available = True
|
||||||
|
except ImportError:
|
||||||
|
multi_series_available = False
|
||||||
|
|
||||||
|
# Scan using MultiDongle (existing method)
|
||||||
devices = MultiDongle.scan_devices()
|
devices = MultiDongle.scan_devices()
|
||||||
|
|
||||||
if devices:
|
if devices:
|
||||||
# Add detected devices to the list
|
# Group devices by series for better organization
|
||||||
|
series_groups = {}
|
||||||
for device in devices:
|
for device in devices:
|
||||||
port_id = device['port_id']
|
|
||||||
series = device['series']
|
series = device['series']
|
||||||
self.dongles_list.addItem(f"{series} Dongle - Port {port_id}")
|
if series not in series_groups:
|
||||||
|
series_groups[series] = []
|
||||||
|
series_groups[series].append(device)
|
||||||
|
|
||||||
# Add summary item
|
# Add header for device listing
|
||||||
self.dongles_list.addItem(f"Total: {len(devices)} device(s) detected")
|
self.dongles_list.addItem("=== Detected Kneron Dongles ===")
|
||||||
|
|
||||||
# Store device info for later use
|
# Display devices grouped by series
|
||||||
|
for series, device_list in series_groups.items():
|
||||||
|
# Add series header with capabilities
|
||||||
|
if multi_series_available:
|
||||||
|
# Find GOPS capacity for this series
|
||||||
|
gops_capacity = "Unknown"
|
||||||
|
for product_id, spec in DongleSeriesSpec.SERIES_SPECS.items():
|
||||||
|
if spec["name"] == series:
|
||||||
|
gops_capacity = f"{spec['gops']} GOPS"
|
||||||
|
break
|
||||||
|
|
||||||
|
series_header = f"{series} Series ({gops_capacity}):"
|
||||||
|
else:
|
||||||
|
series_header = f"{series} Series:"
|
||||||
|
|
||||||
|
self.dongles_list.addItem(series_header)
|
||||||
|
|
||||||
|
# Add individual devices
|
||||||
|
for device in device_list:
|
||||||
|
port_id = device['port_id']
|
||||||
|
device_item = f" Port {port_id}"
|
||||||
|
if 'device_descriptor' in device:
|
||||||
|
desc = device['device_descriptor']
|
||||||
|
if hasattr(desc, 'product_id'):
|
||||||
|
product_id = hex(desc.product_id)
|
||||||
|
device_item += f" (ID: {product_id})"
|
||||||
|
|
||||||
|
self.dongles_list.addItem(device_item)
|
||||||
|
|
||||||
|
# Add multi-series information
|
||||||
|
if multi_series_available and len(series_groups) > 1:
|
||||||
|
self.dongles_list.addItem("")
|
||||||
|
self.dongles_list.addItem("Multi-Series Mode Available!")
|
||||||
|
self.dongles_list.addItem(" Different series can work together for")
|
||||||
|
self.dongles_list.addItem(" improved performance and load balancing.")
|
||||||
|
|
||||||
|
# Calculate total potential GOPS
|
||||||
|
total_gops = 0
|
||||||
|
for series, device_list in series_groups.items():
|
||||||
|
for product_id, spec in DongleSeriesSpec.SERIES_SPECS.items():
|
||||||
|
if spec["name"] == series:
|
||||||
|
total_gops += spec["gops"] * len(device_list)
|
||||||
|
break
|
||||||
|
|
||||||
|
if total_gops > 0:
|
||||||
|
self.dongles_list.addItem(f" Total Combined GOPS: {total_gops}")
|
||||||
|
|
||||||
|
# Add configuration options
|
||||||
|
self.dongles_list.addItem("")
|
||||||
|
self.dongles_list.addItem("=== Configuration Options ===")
|
||||||
|
|
||||||
|
if len(series_groups) > 1 and multi_series_available:
|
||||||
|
self.dongles_list.addItem("Configure Multi-Series Mapping:")
|
||||||
|
self.dongles_list.addItem(" Enable multi-series mode in model")
|
||||||
|
self.dongles_list.addItem(" properties to use mixed dongle types.")
|
||||||
|
else:
|
||||||
|
self.dongles_list.addItem("Single-Series Configuration:")
|
||||||
|
self.dongles_list.addItem(" All detected dongles are same series.")
|
||||||
|
self.dongles_list.addItem(" Standard mode will be used.")
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
self.dongles_list.addItem("")
|
||||||
|
self.dongles_list.addItem(f"Summary: {len(devices)} device(s), {len(series_groups)} series type(s)")
|
||||||
|
|
||||||
|
# Store enhanced device info
|
||||||
self.detected_devices = devices
|
self.detected_devices = devices
|
||||||
|
self.detected_series_groups = series_groups
|
||||||
|
|
||||||
|
# Store multi-series availability for other methods
|
||||||
|
self.multi_series_available = multi_series_available
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.dongles_list.addItem("No Kneron devices detected")
|
self.dongles_list.addItem("No Kneron devices detected")
|
||||||
|
self.dongles_list.addItem("")
|
||||||
|
self.dongles_list.addItem("Troubleshooting:")
|
||||||
|
self.dongles_list.addItem("- Check USB connections")
|
||||||
|
self.dongles_list.addItem("- Ensure dongles are powered")
|
||||||
|
self.dongles_list.addItem("- Try different USB ports")
|
||||||
|
self.dongles_list.addItem("- Check device drivers")
|
||||||
|
|
||||||
self.detected_devices = []
|
self.detected_devices = []
|
||||||
|
self.detected_series_groups = {}
|
||||||
|
self.multi_series_available = multi_series_available
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Fallback to simulation if scanning fails
|
# Enhanced fallback with multi-series simulation
|
||||||
self.dongles_list.addItem("Device scanning failed - using simulation")
|
self.dongles_list.addItem("Device scanning failed - using simulation")
|
||||||
self.dongles_list.addItem("Simulated KL520 Dongle - Port 28")
|
self.dongles_list.addItem("")
|
||||||
self.dongles_list.addItem("Simulated KL720 Dongle - Port 32")
|
self.dongles_list.addItem("=== Simulated Devices ===")
|
||||||
self.detected_devices = []
|
self.dongles_list.addItem("KL520 Series (3 GOPS):")
|
||||||
|
self.dongles_list.addItem(" Port 28 (ID: 0x100)")
|
||||||
|
self.dongles_list.addItem("KL720 Series (28 GOPS):")
|
||||||
|
self.dongles_list.addItem(" Port 32 (ID: 0x720)")
|
||||||
|
self.dongles_list.addItem("")
|
||||||
|
self.dongles_list.addItem("Multi-Series Mode Available!")
|
||||||
|
self.dongles_list.addItem(" Total Combined GOPS: 31")
|
||||||
|
self.dongles_list.addItem("")
|
||||||
|
self.dongles_list.addItem("Summary: 2 device(s), 2 series type(s)")
|
||||||
|
|
||||||
|
# Create simulated device data
|
||||||
|
self.detected_devices = [
|
||||||
|
{'port_id': 28, 'series': 'KL520'},
|
||||||
|
{'port_id': 32, 'series': 'KL720'}
|
||||||
|
]
|
||||||
|
self.detected_series_groups = {
|
||||||
|
'KL520': [{'port_id': 28, 'series': 'KL520'}],
|
||||||
|
'KL720': [{'port_id': 32, 'series': 'KL720'}]
|
||||||
|
}
|
||||||
|
self.multi_series_available = True
|
||||||
|
|
||||||
# Print error for debugging
|
# Print error for debugging
|
||||||
print(f"Dongle detection error: {str(e)}")
|
print(f"Dongle detection error: {str(e)}")
|
||||||
@ -1779,6 +2044,243 @@ class IntegratedPipelineDashboard(QMainWindow):
|
|||||||
"""
|
"""
|
||||||
return [device['port_id'] for device in self.get_detected_devices()]
|
return [device['port_id'] for device in self.get_detected_devices()]
|
||||||
|
|
||||||
|
def create_port_mapping_widget(self, node):
|
||||||
|
"""Create port mapping widget for multi-series configuration."""
|
||||||
|
try:
|
||||||
|
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
|
QLabel, QPushButton, QComboBox, QTableWidget,
|
||||||
|
QTableWidgetItem, QHeaderView)
|
||||||
|
|
||||||
|
# Main container widget
|
||||||
|
container = QWidget()
|
||||||
|
container.setStyleSheet("""
|
||||||
|
QWidget {
|
||||||
|
background-color: #1e1e2e;
|
||||||
|
border: 1px solid #45475a;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
layout = QVBoxLayout(container)
|
||||||
|
layout.setContentsMargins(8, 8, 8, 8)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title_label = QLabel("Port ID to Series Mapping")
|
||||||
|
title_label.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
color: #f9e2af;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
layout.addWidget(title_label)
|
||||||
|
|
||||||
|
# Get detected devices
|
||||||
|
series_groups = getattr(self, 'detected_series_groups', {})
|
||||||
|
detected_devices = getattr(self, 'detected_devices', [])
|
||||||
|
|
||||||
|
if not detected_devices:
|
||||||
|
# Show message if no devices detected
|
||||||
|
no_devices_label = QLabel("No devices detected. Use 'Detect Dongles' button above.")
|
||||||
|
no_devices_label.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
color: #f38ba8;
|
||||||
|
font-size: 11px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
layout.addWidget(no_devices_label)
|
||||||
|
return container
|
||||||
|
|
||||||
|
# Create mapping table
|
||||||
|
if len(series_groups) > 1:
|
||||||
|
# Multiple series detected - show mapping table
|
||||||
|
table = QTableWidget()
|
||||||
|
table.setColumnCount(3)
|
||||||
|
table.setHorizontalHeaderLabels(["Port ID", "Detected Series", "Assign To"])
|
||||||
|
table.setRowCount(len(detected_devices))
|
||||||
|
|
||||||
|
# Style the table
|
||||||
|
table.setStyleSheet("""
|
||||||
|
QTableWidget {
|
||||||
|
background-color: #313244;
|
||||||
|
gridline-color: #45475a;
|
||||||
|
color: #cdd6f4;
|
||||||
|
border: 1px solid #45475a;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
QTableWidget::item {
|
||||||
|
padding: 5px;
|
||||||
|
border-bottom: 1px solid #45475a;
|
||||||
|
}
|
||||||
|
QTableWidget::item:selected {
|
||||||
|
background-color: #89b4fa;
|
||||||
|
}
|
||||||
|
QHeaderView::section {
|
||||||
|
background-color: #45475a;
|
||||||
|
color: #f9e2af;
|
||||||
|
padding: 5px;
|
||||||
|
border: none;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Get current port mapping from node
|
||||||
|
current_mapping = node.get_property('port_mapping') if hasattr(node, 'get_property') else {}
|
||||||
|
|
||||||
|
# Populate table
|
||||||
|
available_series = list(series_groups.keys())
|
||||||
|
for i, device in enumerate(detected_devices):
|
||||||
|
port_id = device['port_id']
|
||||||
|
detected_series = device['series']
|
||||||
|
|
||||||
|
# Port ID column (read-only)
|
||||||
|
port_item = QTableWidgetItem(str(port_id))
|
||||||
|
port_item.setFlags(port_item.flags() & ~0x02) # Make read-only
|
||||||
|
table.setItem(i, 0, port_item)
|
||||||
|
|
||||||
|
# Detected Series column (read-only)
|
||||||
|
series_item = QTableWidgetItem(detected_series)
|
||||||
|
series_item.setFlags(series_item.flags() & ~0x02) # Make read-only
|
||||||
|
table.setItem(i, 1, series_item)
|
||||||
|
|
||||||
|
# Assignment combo box
|
||||||
|
combo = QComboBox()
|
||||||
|
combo.addItems(['Auto'] + available_series)
|
||||||
|
|
||||||
|
# Set current mapping
|
||||||
|
if str(port_id) in current_mapping:
|
||||||
|
mapped_series = current_mapping[str(port_id)]
|
||||||
|
if mapped_series in available_series:
|
||||||
|
combo.setCurrentText(mapped_series)
|
||||||
|
else:
|
||||||
|
combo.setCurrentText('Auto')
|
||||||
|
else:
|
||||||
|
combo.setCurrentText('Auto')
|
||||||
|
|
||||||
|
# Style combo box
|
||||||
|
combo.setStyleSheet("""
|
||||||
|
QComboBox {
|
||||||
|
background-color: #45475a;
|
||||||
|
color: #cdd6f4;
|
||||||
|
border: 1px solid #585b70;
|
||||||
|
padding: 3px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
QComboBox:hover {
|
||||||
|
border-color: #74c7ec;
|
||||||
|
}
|
||||||
|
QComboBox::drop-down {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
QComboBox::down-arrow {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
def make_mapping_handler(port, combo_widget):
|
||||||
|
def on_mapping_change(series_name):
|
||||||
|
# Update node property
|
||||||
|
if hasattr(node, 'set_property'):
|
||||||
|
current_mapping = node.get_property('port_mapping') if hasattr(node, 'get_property') else {}
|
||||||
|
if series_name == 'Auto':
|
||||||
|
# Remove explicit mapping, let auto-detection handle it
|
||||||
|
current_mapping.pop(str(port), None)
|
||||||
|
else:
|
||||||
|
current_mapping[str(port)] = series_name
|
||||||
|
node.set_property('port_mapping', current_mapping)
|
||||||
|
print(f"Port {port} mapped to {series_name}")
|
||||||
|
return on_mapping_change
|
||||||
|
|
||||||
|
combo.currentTextChanged.connect(make_mapping_handler(port_id, combo))
|
||||||
|
table.setCellWidget(i, 2, combo)
|
||||||
|
|
||||||
|
# Adjust column widths
|
||||||
|
table.horizontalHeader().setStretchLastSection(True)
|
||||||
|
table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
||||||
|
table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
|
||||||
|
table.setMaximumHeight(150)
|
||||||
|
|
||||||
|
layout.addWidget(table)
|
||||||
|
|
||||||
|
# Add configuration button
|
||||||
|
config_button = QPushButton("Advanced Configuration")
|
||||||
|
config_button.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: #89b4fa;
|
||||||
|
color: #1e1e2e;
|
||||||
|
border: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: #74c7ec;
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background-color: #585b70;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
def open_multi_series_config():
|
||||||
|
try:
|
||||||
|
from ui.dialogs.multi_series_config import MultiSeriesConfigDialog
|
||||||
|
dialog = MultiSeriesConfigDialog()
|
||||||
|
|
||||||
|
# Pre-populate with current detected devices
|
||||||
|
if hasattr(dialog, 'set_detected_devices'):
|
||||||
|
dialog.set_detected_devices(detected_devices, series_groups)
|
||||||
|
|
||||||
|
if dialog.exec_() == dialog.Accepted:
|
||||||
|
config = dialog.get_configuration()
|
||||||
|
# Update node properties with configuration
|
||||||
|
if hasattr(node, 'set_property') and config:
|
||||||
|
for key, value in config.items():
|
||||||
|
node.set_property(key, value)
|
||||||
|
# Refresh properties panel
|
||||||
|
self.update_node_properties_panel(node)
|
||||||
|
print("Multi-series configuration updated")
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"Multi-series config dialog not available: {e}")
|
||||||
|
|
||||||
|
config_button.clicked.connect(open_multi_series_config)
|
||||||
|
layout.addWidget(config_button)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Single series detected - show info message
|
||||||
|
single_series = list(series_groups.keys())[0] if series_groups else "Unknown"
|
||||||
|
info_label = QLabel(f"All devices are {single_series} series. Multi-series mapping not needed.")
|
||||||
|
info_label.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
color: #94e2d5;
|
||||||
|
font-size: 11px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
layout.addWidget(info_label)
|
||||||
|
|
||||||
|
return container
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating port mapping widget: {e}")
|
||||||
|
# Return simple label as fallback
|
||||||
|
from PyQt5.QtWidgets import QLabel
|
||||||
|
fallback_label = QLabel("Port mapping configuration unavailable")
|
||||||
|
fallback_label.setStyleSheet("color: #f38ba8; padding: 10px;")
|
||||||
|
return fallback_label
|
||||||
|
|
||||||
def get_device_by_port(self, port_id):
|
def get_device_by_port(self, port_id):
|
||||||
"""
|
"""
|
||||||
Get device information by port ID.
|
Get device information by port ID.
|
||||||
|
|||||||
447
utils/multi_series_setup.py
Normal file
447
utils/multi_series_setup.py
Normal file
@ -0,0 +1,447 @@
|
|||||||
|
"""
|
||||||
|
Multi-Series Setup Utility
|
||||||
|
|
||||||
|
This utility helps users set up the proper folder structure and configuration
|
||||||
|
for multi-series dongle inference.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Create recommended folder structure
|
||||||
|
- Validate existing folder structure
|
||||||
|
- Generate example configuration files
|
||||||
|
- Provide setup guidance and troubleshooting
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python utils/multi_series_setup.py create-structure --path "C:/MyAssets"
|
||||||
|
python utils/multi_series_setup.py validate --path "C:/MyAssets"
|
||||||
|
python utils/multi_series_setup.py help
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
from typing import List, Tuple, Dict
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class MultiSeriesSetup:
|
||||||
|
"""Utility class for multi-series setup operations"""
|
||||||
|
|
||||||
|
SUPPORTED_SERIES = ['520', '720', '630', '730', '540']
|
||||||
|
REQUIRED_FW_FILES = ['fw_scpu.bin', 'fw_ncpu.bin']
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_folder_structure(base_path: str, series_list: List[str] = None) -> bool:
|
||||||
|
"""
|
||||||
|
Create the recommended folder structure for multi-series assets
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_path: Root path where assets folder should be created
|
||||||
|
series_list: List of series to create folders for
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: Success status
|
||||||
|
"""
|
||||||
|
if series_list is None:
|
||||||
|
series_list = MultiSeriesSetup.SUPPORTED_SERIES
|
||||||
|
|
||||||
|
try:
|
||||||
|
assets_path = os.path.join(base_path, 'Assets')
|
||||||
|
firmware_path = os.path.join(assets_path, 'Firmware')
|
||||||
|
models_path = os.path.join(assets_path, 'Models')
|
||||||
|
|
||||||
|
# Create main directories
|
||||||
|
os.makedirs(firmware_path, exist_ok=True)
|
||||||
|
os.makedirs(models_path, exist_ok=True)
|
||||||
|
|
||||||
|
print(f"✓ Created main directories at: {assets_path}")
|
||||||
|
|
||||||
|
# Create series-specific directories
|
||||||
|
created_series = []
|
||||||
|
for series in series_list:
|
||||||
|
series_name = f'KL{series}'
|
||||||
|
fw_dir = os.path.join(firmware_path, series_name)
|
||||||
|
model_dir = os.path.join(models_path, series_name)
|
||||||
|
|
||||||
|
os.makedirs(fw_dir, exist_ok=True)
|
||||||
|
os.makedirs(model_dir, exist_ok=True)
|
||||||
|
created_series.append(series_name)
|
||||||
|
|
||||||
|
print(f"✓ Created series directories: {', '.join(created_series)}")
|
||||||
|
|
||||||
|
# Create README file explaining the structure
|
||||||
|
readme_content = MultiSeriesSetup._generate_readme_content()
|
||||||
|
readme_path = os.path.join(assets_path, 'README.md')
|
||||||
|
with open(readme_path, 'w') as f:
|
||||||
|
f.write(readme_content)
|
||||||
|
|
||||||
|
print(f"✓ Created README file: {readme_path}")
|
||||||
|
|
||||||
|
# Create example configuration
|
||||||
|
config_example = MultiSeriesSetup._generate_example_config(assets_path, series_list)
|
||||||
|
config_path = os.path.join(assets_path, 'example_config.json')
|
||||||
|
with open(config_path, 'w') as f:
|
||||||
|
json.dump(config_example, f, indent=2)
|
||||||
|
|
||||||
|
print(f"✓ Created example configuration: {config_path}")
|
||||||
|
|
||||||
|
print(f"\n🎉 Multi-series folder structure created successfully!")
|
||||||
|
print(f"📁 Assets folder: {assets_path}")
|
||||||
|
print("\n📋 Next steps:")
|
||||||
|
print("1. Copy your firmware files to the appropriate Firmware/KLxxx/ folders")
|
||||||
|
print("2. Copy your model files to the appropriate Models/KLxxx/ folders")
|
||||||
|
print("3. Configure your model node to use this Assets folder")
|
||||||
|
print("4. Enable multi-series mode and select desired series")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error creating folder structure: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_folder_structure(assets_path: str) -> Tuple[bool, List[str]]:
|
||||||
|
"""
|
||||||
|
Validate an existing folder structure for multi-series configuration
|
||||||
|
|
||||||
|
Args:
|
||||||
|
assets_path: Path to the assets folder
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (is_valid, list_of_issues)
|
||||||
|
"""
|
||||||
|
issues = []
|
||||||
|
|
||||||
|
# Check if assets folder exists
|
||||||
|
if not os.path.exists(assets_path):
|
||||||
|
issues.append(f"Assets folder does not exist: {assets_path}")
|
||||||
|
return False, issues
|
||||||
|
|
||||||
|
# Check for main folders
|
||||||
|
firmware_path = os.path.join(assets_path, 'Firmware')
|
||||||
|
models_path = os.path.join(assets_path, 'Models')
|
||||||
|
|
||||||
|
if not os.path.exists(firmware_path):
|
||||||
|
issues.append(f"Firmware folder missing: {firmware_path}")
|
||||||
|
|
||||||
|
if not os.path.exists(models_path):
|
||||||
|
issues.append(f"Models folder missing: {models_path}")
|
||||||
|
|
||||||
|
if issues:
|
||||||
|
return False, issues
|
||||||
|
|
||||||
|
# Check series folders and contents
|
||||||
|
found_series = []
|
||||||
|
|
||||||
|
for item in os.listdir(firmware_path):
|
||||||
|
if item.startswith('KL') and os.path.isdir(os.path.join(firmware_path, item)):
|
||||||
|
series_name = item
|
||||||
|
series_fw_path = os.path.join(firmware_path, series_name)
|
||||||
|
series_model_path = os.path.join(models_path, series_name)
|
||||||
|
|
||||||
|
# Check if corresponding model folder exists
|
||||||
|
if not os.path.exists(series_model_path):
|
||||||
|
issues.append(f"Models folder missing for {series_name}: {series_model_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check firmware files
|
||||||
|
fw_issues = []
|
||||||
|
for fw_file in MultiSeriesSetup.REQUIRED_FW_FILES:
|
||||||
|
fw_file_path = os.path.join(series_fw_path, fw_file)
|
||||||
|
if not os.path.exists(fw_file_path):
|
||||||
|
fw_issues.append(f"{fw_file} missing")
|
||||||
|
|
||||||
|
if fw_issues:
|
||||||
|
issues.append(f"{series_name} firmware issues: {', '.join(fw_issues)}")
|
||||||
|
|
||||||
|
# Check for model files
|
||||||
|
model_files = [f for f in os.listdir(series_model_path) if f.endswith('.nef')]
|
||||||
|
if not model_files:
|
||||||
|
issues.append(f"{series_name} has no .nef model files in {series_model_path}")
|
||||||
|
|
||||||
|
if not fw_issues and model_files:
|
||||||
|
found_series.append(series_name)
|
||||||
|
|
||||||
|
if not found_series:
|
||||||
|
issues.append("No valid series configurations found")
|
||||||
|
|
||||||
|
is_valid = len(issues) == 0
|
||||||
|
|
||||||
|
# Print validation results
|
||||||
|
if is_valid:
|
||||||
|
print(f"✅ Validation passed!")
|
||||||
|
print(f"📁 Assets folder: {assets_path}")
|
||||||
|
print(f"🎯 Valid series found: {', '.join(found_series)}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Validation failed with {len(issues)} issues:")
|
||||||
|
for i, issue in enumerate(issues, 1):
|
||||||
|
print(f" {i}. {issue}")
|
||||||
|
|
||||||
|
return is_valid, issues
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def list_available_series(assets_path: str) -> Dict[str, Dict[str, any]]:
|
||||||
|
"""
|
||||||
|
List all available and configured series in the assets folder
|
||||||
|
|
||||||
|
Args:
|
||||||
|
assets_path: Path to the assets folder
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with series information
|
||||||
|
"""
|
||||||
|
series_info = {}
|
||||||
|
|
||||||
|
if not os.path.exists(assets_path):
|
||||||
|
return series_info
|
||||||
|
|
||||||
|
firmware_path = os.path.join(assets_path, 'Firmware')
|
||||||
|
models_path = os.path.join(assets_path, 'Models')
|
||||||
|
|
||||||
|
if not os.path.exists(firmware_path) or not os.path.exists(models_path):
|
||||||
|
return series_info
|
||||||
|
|
||||||
|
for item in os.listdir(firmware_path):
|
||||||
|
if item.startswith('KL') and os.path.isdir(os.path.join(firmware_path, item)):
|
||||||
|
series_name = item
|
||||||
|
series_fw_path = os.path.join(firmware_path, series_name)
|
||||||
|
series_model_path = os.path.join(models_path, series_name)
|
||||||
|
|
||||||
|
# Check firmware files
|
||||||
|
fw_files = {}
|
||||||
|
for fw_file in MultiSeriesSetup.REQUIRED_FW_FILES:
|
||||||
|
fw_file_path = os.path.join(series_fw_path, fw_file)
|
||||||
|
fw_files[fw_file] = os.path.exists(fw_file_path)
|
||||||
|
|
||||||
|
# Check model files
|
||||||
|
model_files = []
|
||||||
|
if os.path.exists(series_model_path):
|
||||||
|
model_files = [f for f in os.listdir(series_model_path) if f.endswith('.nef')]
|
||||||
|
|
||||||
|
# Determine status
|
||||||
|
fw_complete = all(fw_files.values())
|
||||||
|
has_models = len(model_files) > 0
|
||||||
|
|
||||||
|
if fw_complete and has_models:
|
||||||
|
status = "✅ Ready"
|
||||||
|
elif fw_complete:
|
||||||
|
status = "⚠️ Missing models"
|
||||||
|
elif has_models:
|
||||||
|
status = "⚠️ Missing firmware"
|
||||||
|
else:
|
||||||
|
status = "❌ Incomplete"
|
||||||
|
|
||||||
|
series_info[series_name] = {
|
||||||
|
'status': status,
|
||||||
|
'firmware_files': fw_files,
|
||||||
|
'model_files': model_files,
|
||||||
|
'firmware_path': series_fw_path,
|
||||||
|
'models_path': series_model_path
|
||||||
|
}
|
||||||
|
|
||||||
|
return series_info
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _generate_readme_content() -> str:
|
||||||
|
"""Generate README content for the assets folder"""
|
||||||
|
return '''
|
||||||
|
# Multi-Series Assets Folder Structure
|
||||||
|
|
||||||
|
This folder contains firmware and models organized by dongle series for multi-series inference.
|
||||||
|
|
||||||
|
## Structure:
|
||||||
|
```
|
||||||
|
Assets/
|
||||||
|
├── Firmware/
|
||||||
|
│ ├── KL520/
|
||||||
|
│ │ ├── fw_scpu.bin
|
||||||
|
│ │ └── fw_ncpu.bin
|
||||||
|
│ ├── KL720/
|
||||||
|
│ │ ├── fw_scpu.bin
|
||||||
|
│ │ └── fw_ncpu.bin
|
||||||
|
│ └── [other series...]
|
||||||
|
└── Models/
|
||||||
|
├── KL520/
|
||||||
|
│ └── [model.nef files]
|
||||||
|
├── KL720/
|
||||||
|
│ └── [model.nef files]
|
||||||
|
└── [other series...]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage:
|
||||||
|
1. Place firmware files (fw_scpu.bin, fw_ncpu.bin) in the appropriate series subfolder under Firmware/
|
||||||
|
2. Place model files (.nef) in the appropriate series subfolder under Models/
|
||||||
|
3. Configure your model node to use this Assets folder in multi-series mode
|
||||||
|
4. Select which series to enable in the model node properties
|
||||||
|
|
||||||
|
## Supported Series:
|
||||||
|
- **KL520**: Entry-level performance (3 GOPS)
|
||||||
|
- **KL720**: Mid-range performance (28 GOPS)
|
||||||
|
- **KL630**: High performance (400 GOPS)
|
||||||
|
- **KL730**: Very high performance (1600 GOPS)
|
||||||
|
- **KL540**: Specialized performance (800 GOPS)
|
||||||
|
|
||||||
|
## Performance Benefits:
|
||||||
|
The multi-series system automatically load balances inference across all enabled series
|
||||||
|
based on their GOPS capacity for optimal performance. You can expect:
|
||||||
|
|
||||||
|
- Higher overall throughput by utilizing multiple dongle types simultaneously
|
||||||
|
- Automatic load balancing based on dongle capabilities
|
||||||
|
- Seamless failover if one series becomes unavailable
|
||||||
|
- Scalable performance as you add more dongles
|
||||||
|
|
||||||
|
## Validation:
|
||||||
|
Run `python utils/multi_series_setup.py validate --path <this_folder>` to validate your configuration.
|
||||||
|
|
||||||
|
## Troubleshooting:
|
||||||
|
- Ensure all firmware files are exactly named `fw_scpu.bin` and `fw_ncpu.bin`
|
||||||
|
- Model files must have `.nef` extension
|
||||||
|
- Each series must have both firmware and at least one model file
|
||||||
|
- Check file permissions and accessibility
|
||||||
|
'''.strip()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _generate_example_config(assets_path: str, series_list: List[str]) -> Dict:
|
||||||
|
"""Generate example configuration for model node"""
|
||||||
|
return {
|
||||||
|
"model_node_properties": {
|
||||||
|
"multi_series_mode": True,
|
||||||
|
"assets_folder": assets_path,
|
||||||
|
"enabled_series": series_list[:2], # Enable first two series by default
|
||||||
|
"max_queue_size": 100,
|
||||||
|
"result_buffer_size": 1000,
|
||||||
|
"batch_size": 1
|
||||||
|
},
|
||||||
|
"expected_performance": {
|
||||||
|
"total_gops": sum([
|
||||||
|
{"520": 3, "720": 28, "630": 400, "730": 1600, "540": 800}.get(series, 0)
|
||||||
|
for series in series_list[:2]
|
||||||
|
]),
|
||||||
|
"load_balancing": "automatic",
|
||||||
|
"expected_fps_improvement": "2-5x compared to single series"
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"This is an example configuration",
|
||||||
|
"Adjust enabled_series based on your available dongles",
|
||||||
|
"Higher queue sizes may improve performance but use more memory",
|
||||||
|
"Monitor system resources when using multiple series"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main CLI interface"""
|
||||||
|
parser = argparse.ArgumentParser(description='Multi-Series Dongle Setup Utility')
|
||||||
|
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
||||||
|
|
||||||
|
# Create structure command
|
||||||
|
create_parser = subparsers.add_parser('create-structure', help='Create folder structure')
|
||||||
|
create_parser.add_argument('--path', required=True, help='Base path for assets folder')
|
||||||
|
create_parser.add_argument('--series', nargs='*', default=None, help='Series to set up (default: all)')
|
||||||
|
|
||||||
|
# Validate command
|
||||||
|
validate_parser = subparsers.add_parser('validate', help='Validate existing structure')
|
||||||
|
validate_parser.add_argument('--path', required=True, help='Path to assets folder')
|
||||||
|
|
||||||
|
# List command
|
||||||
|
list_parser = subparsers.add_parser('list', help='List available series')
|
||||||
|
list_parser.add_argument('--path', required=True, help='Path to assets folder')
|
||||||
|
|
||||||
|
# Help command
|
||||||
|
help_parser = subparsers.add_parser('help', help='Show detailed help')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.command == 'create-structure':
|
||||||
|
series_list = args.series if args.series else None
|
||||||
|
MultiSeriesSetup.create_folder_structure(args.path, series_list)
|
||||||
|
|
||||||
|
elif args.command == 'validate':
|
||||||
|
is_valid, issues = MultiSeriesSetup.validate_folder_structure(args.path)
|
||||||
|
sys.exit(0 if is_valid else 1)
|
||||||
|
|
||||||
|
elif args.command == 'list':
|
||||||
|
series_info = MultiSeriesSetup.list_available_series(args.path)
|
||||||
|
|
||||||
|
if not series_info:
|
||||||
|
print(f"❌ No series found in {args.path}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"📁 Series configuration in {args.path}:\n")
|
||||||
|
|
||||||
|
for series_name, info in series_info.items():
|
||||||
|
print(f" {series_name}: {info['status']}")
|
||||||
|
print(f" 📁 Firmware: {info['firmware_path']}")
|
||||||
|
print(f" 📁 Models: {info['models_path']}")
|
||||||
|
|
||||||
|
if info['model_files']:
|
||||||
|
print(f" 📄 Model files: {', '.join(info['model_files'])}")
|
||||||
|
|
||||||
|
fw_issues = [fw for fw, exists in info['firmware_files'].items() if not exists]
|
||||||
|
if fw_issues:
|
||||||
|
print(f" ⚠️ Missing firmware: {', '.join(fw_issues)}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
elif args.command == 'help':
|
||||||
|
print("""
|
||||||
|
Multi-Series Dongle Setup Help
|
||||||
|
=============================
|
||||||
|
|
||||||
|
This utility helps you set up and manage multi-series dongle configurations
|
||||||
|
for improved inference performance.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
---------
|
||||||
|
|
||||||
|
create-structure --path <path> [--series KL520 KL720 ...]
|
||||||
|
Creates the recommended folder structure for multi-series assets.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
python utils/multi_series_setup.py create-structure --path "C:/MyAssets"
|
||||||
|
python utils/multi_series_setup.py create-structure --path "C:/MyAssets" --series 520 720
|
||||||
|
|
||||||
|
validate --path <path>
|
||||||
|
Validates an existing assets folder structure.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
python utils/multi_series_setup.py validate --path "C:/MyAssets/Assets"
|
||||||
|
|
||||||
|
list --path <path>
|
||||||
|
Lists all available series and their status in an assets folder.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
python utils/multi_series_setup.py list --path "C:/MyAssets/Assets"
|
||||||
|
|
||||||
|
Setup Workflow:
|
||||||
|
--------------
|
||||||
|
1. Create folder structure: create-structure --path "C:/MyProject"
|
||||||
|
2. Copy firmware files to Assets/Firmware/KLxxx/ folders
|
||||||
|
3. Copy model files to Assets/Models/KLxxx/ folders
|
||||||
|
4. Validate configuration: validate --path "C:/MyProject/Assets"
|
||||||
|
5. Configure model node in UI to use Assets folder
|
||||||
|
6. Enable multi-series mode and select desired series
|
||||||
|
|
||||||
|
Performance Benefits:
|
||||||
|
-------------------
|
||||||
|
- 2-5x throughput improvement with multiple series
|
||||||
|
- Automatic load balancing based on dongle GOPS
|
||||||
|
- Seamless scaling as you add more dongles
|
||||||
|
- Fault tolerance if some dongles become unavailable
|
||||||
|
|
||||||
|
Troubleshooting:
|
||||||
|
---------------
|
||||||
|
- Ensure exact firmware file names: fw_scpu.bin, fw_ncpu.bin
|
||||||
|
- Model files must have .nef extension
|
||||||
|
- Check file permissions and paths
|
||||||
|
- Verify dongle connectivity with single-series mode first
|
||||||
|
- Use validate command to check configuration
|
||||||
|
|
||||||
|
For more help, see Assets/README.md after creating the structure.
|
||||||
|
""")
|
||||||
|
|
||||||
|
else:
|
||||||
|
parser.print_help()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Loading…
x
Reference in New Issue
Block a user