cluster4npu/ui/dialogs/multi_series_config.py

1207 lines
48 KiB
Python

"""
Multi-Series Configuration Dialog
This dialog allows users to configure multi-series dongle inference with different
dongle models, specify model and firmware paths, and map port IDs to specific series.
Features:
- Series selection and configuration
- Model and firmware path specification (folder or individual files)
- Port ID mapping for different series
- Validation and preview functionality
Usage:
from ui.dialogs.multi_series_config import MultiSeriesConfigDialog
dialog = MultiSeriesConfigDialog(parent=self)
if dialog.exec_() == dialog.Accepted:
config = dialog.get_configuration()
"""
import os
import json
from typing import Dict, List, Tuple, Any, Optional
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTableWidget,
QTableWidgetItem, QGroupBox, QFormLayout, QLineEdit, QCheckBox,
QFileDialog, QMessageBox, QTabWidget, QWidget, QTextEdit, QComboBox,
QSpinBox, QScrollArea, QGridLayout, QSplitter, QHeaderView, QFrame
)
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QFont
# Import multi-series utilities
try:
from utils.multi_series_setup import MultiSeriesSetup
SETUP_UTILS_AVAILABLE = True
except ImportError:
SETUP_UTILS_AVAILABLE = False
try:
from multi_series_dongle_manager import DongleSeriesSpec
DONGLE_MANAGER_AVAILABLE = True
except ImportError:
DONGLE_MANAGER_AVAILABLE = False
class MultiSeriesConfigDialog(QDialog):
"""
Dialog for configuring multi-series dongle inference.
Allows users to:
- Select which dongle series to enable
- Configure model and firmware paths (folder-based or individual files)
- Map port IDs to specific series
- Preview and validate configuration
"""
# Signals
configuration_changed = pyqtSignal(dict)
def __init__(self, parent=None, current_config: Dict[str, Any] = None):
super().__init__(parent)
self.current_config = current_config or {}
self.detected_devices = [] # Will be populated by parent
self.language = self.current_config.get('language', 'en') # 'en' or 'zh'
# Text translations
self.texts = {
'en': {
'title': 'Multi-Series Dongle Configuration',
'series_selection': 'Series Selection',
'enable_series': 'Enable Series',
'model_firmware': 'Model & Firmware Configuration',
'port_mapping': 'Port ID Mapping',
'validation': 'Configuration Validation',
'assets_folder': 'Assets Folder',
'browse_folder': 'Browse Folder...',
'individual_paths': 'Individual Paths',
'model_path': 'Model Path',
'firmware_scpu': 'SCPU Firmware',
'firmware_ncpu': 'NCPU Firmware',
'browse_file': 'Browse...',
'port_id': 'Port ID',
'assigned_series': 'Assigned Series',
'auto_assign': 'Auto Assign',
'validate_config': 'Validate Configuration',
'preview_config': 'Preview Configuration',
'save_config': 'Save Configuration',
'load_config': 'Load Configuration',
'ok': 'OK',
'cancel': 'Cancel',
'help': 'Help',
'status': 'Status',
'gops': 'GOPS',
'description': 'Description',
'folder_mode': 'Use folder structure (recommended)',
'individual_mode': 'Specify individual files',
'detected_devices': 'Detected Devices',
'no_devices': 'No devices detected',
'click_detect': 'Click "Detect Devices" to scan',
'detect_devices': 'Detect Devices',
'config_valid': 'Configuration is valid',
'config_invalid': 'Configuration has issues',
'select_folder': 'Select Assets Folder',
'select_model': 'Select Model File',
'select_firmware': 'Select Firmware File',
'error': 'Error',
'warning': 'Warning',
'info': 'Information'
},
'zh': {
'title': '多系列加密狗配置',
'series_selection': '系列選擇',
'enable_series': '啟用系列',
'model_firmware': '模型與韌體配置',
'port_mapping': '連接埠ID映射',
'validation': '配置驗證',
'assets_folder': '資源資料夾',
'browse_folder': '瀏覽資料夾...',
'individual_paths': '個別路徑',
'model_path': '模型路徑',
'firmware_scpu': 'SCPU 韌體',
'firmware_ncpu': 'NCPU 韌體',
'browse_file': '瀏覽...',
'port_id': '連接埠ID',
'assigned_series': '指派系列',
'auto_assign': '自動指派',
'validate_config': '驗證配置',
'preview_config': '預覽配置',
'save_config': '儲存配置',
'load_config': '載入配置',
'ok': '確定',
'cancel': '取消',
'help': '說明',
'status': '狀態',
'gops': 'GOPS',
'description': '描述',
'folder_mode': '使用資料夾結構(推薦)',
'individual_mode': '指定個別檔案',
'detected_devices': '偵測到的裝置',
'no_devices': '未偵測到裝置',
'click_detect': '點擊「偵測裝置」進行掃描',
'detect_devices': '偵測裝置',
'config_valid': '配置有效',
'config_invalid': '配置存在問題',
'select_folder': '選擇資源資料夾',
'select_model': '選擇模型檔案',
'select_firmware': '選擇韌體檔案',
'error': '錯誤',
'warning': '警告',
'info': '資訊'
}
}
self.setup_ui()
self.apply_theme()
self.load_current_config()
def t(self, key: str) -> str:
"""Get translated text."""
return self.texts[self.language].get(key, key)
def setup_ui(self):
"""Setup the dialog UI."""
self.setWindowTitle(self.t('title'))
self.setMinimumSize(900, 700)
self.resize(1200, 800)
# Main layout
layout = QVBoxLayout(self)
# Language selector
lang_layout = QHBoxLayout()
lang_layout.addWidget(QLabel("Language:"))
self.language_combo = QComboBox()
self.language_combo.addItems(["English"])
self.language_combo.setCurrentText("English")
self.language_combo.currentTextChanged.connect(self.change_language)
lang_layout.addWidget(self.language_combo)
lang_layout.addStretch()
layout.addLayout(lang_layout)
# Tab widget
self.tab_widget = QTabWidget()
# Series Selection Tab
self.series_tab = self.create_series_selection_tab()
self.tab_widget.addTab(self.series_tab, self.t('series_selection'))
# Model & Firmware Configuration Tab
self.config_tab = self.create_config_tab()
self.tab_widget.addTab(self.config_tab, self.t('model_firmware'))
# Port Mapping Tab
self.mapping_tab = self.create_mapping_tab()
self.tab_widget.addTab(self.mapping_tab, self.t('port_mapping'))
# Validation Tab
self.validation_tab = self.create_validation_tab()
self.tab_widget.addTab(self.validation_tab, self.t('validation'))
layout.addWidget(self.tab_widget)
# Button layout
button_layout = QHBoxLayout()
# Configuration management buttons
self.load_config_btn = QPushButton(self.t('load_config'))
self.load_config_btn.clicked.connect(self.load_configuration_from_file)
button_layout.addWidget(self.load_config_btn)
self.save_config_btn = QPushButton(self.t('save_config'))
self.save_config_btn.clicked.connect(self.save_configuration_to_file)
button_layout.addWidget(self.save_config_btn)
button_layout.addStretch()
# Help button
self.help_btn = QPushButton(self.t('help'))
self.help_btn.clicked.connect(self.show_help)
button_layout.addWidget(self.help_btn)
# Dialog buttons
self.ok_btn = QPushButton(self.t('ok'))
self.ok_btn.clicked.connect(self.accept)
self.ok_btn.setDefault(True)
button_layout.addWidget(self.ok_btn)
self.cancel_btn = QPushButton(self.t('cancel'))
self.cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(self.cancel_btn)
layout.addLayout(button_layout)
def create_series_selection_tab(self) -> QWidget:
"""Create the series selection tab."""
widget = QWidget()
layout = QVBoxLayout(widget)
# Series table
series_group = QGroupBox(self.t('enable_series'))
series_layout = QVBoxLayout(series_group)
self.series_table = QTableWidget()
self.series_table.setColumnCount(5)
self.series_table.setHorizontalHeaderLabels([
self.t('enable_series'), 'Series', self.t('gops'), self.t('status'), self.t('description')
])
# Populate series table
if DONGLE_MANAGER_AVAILABLE:
series_specs = DongleSeriesSpec.SERIES_SPECS
else:
# Fallback specs
series_specs = {
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}
}
self.series_table.setRowCount(len(series_specs))
series_descriptions = {
"KL520": "Entry-level NPU (3 GOPS)" if self.language == 'en' else "入門級NPU (3 GOPS)",
"KL720": "Mid-range NPU (28 GOPS)" if self.language == 'en' else "中階NPU (28 GOPS)",
"KL630": "High-performance NPU (400 GOPS)" if self.language == 'en' else "高性能NPU (400 GOPS)",
"KL730": "Very high-performance NPU (1600 GOPS)" if self.language == 'en' else "超高性能NPU (1600 GOPS)",
"KL540": "Specialized NPU (800 GOPS)" if self.language == 'en' else "專用NPU (800 GOPS)"
}
for i, (product_id, spec) in enumerate(series_specs.items()):
series_name = spec["name"]
# Enable checkbox
enable_checkbox = QCheckBox()
enable_checkbox.setObjectName(f"enable_{series_name}")
self.series_table.setCellWidget(i, 0, enable_checkbox)
# Series name
self.series_table.setItem(i, 1, QTableWidgetItem(series_name))
# GOPS
self.series_table.setItem(i, 2, QTableWidgetItem(str(spec["gops"])))
# Status (will be updated when devices are detected)
status_item = QTableWidgetItem("Not detected" if self.language == 'en' else "未偵測")
status_item.setData(Qt.UserRole, product_id) # Store product_id for reference
self.series_table.setItem(i, 3, status_item)
# Description
description = series_descriptions.get(series_name, "")
self.series_table.setItem(i, 4, QTableWidgetItem(description))
# Make table columns resize properly
header = self.series_table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
header.setSectionResizeMode(3, QHeaderView.ResizeToContents)
header.setSectionResizeMode(4, QHeaderView.Stretch)
series_layout.addWidget(self.series_table)
layout.addWidget(series_group)
# Device detection
detect_group = QGroupBox(self.t('detected_devices'))
detect_layout = QVBoxLayout(detect_group)
# Detect button
detect_btn_layout = QHBoxLayout()
self.detect_devices_btn = QPushButton(self.t('detect_devices'))
self.detect_devices_btn.clicked.connect(self.detect_devices)
detect_btn_layout.addWidget(self.detect_devices_btn)
detect_btn_layout.addStretch()
detect_layout.addLayout(detect_btn_layout)
# Detected devices display
self.detected_devices_text = QTextEdit()
self.detected_devices_text.setReadOnly(True)
self.detected_devices_text.setMaximumHeight(150)
self.detected_devices_text.setText(self.t('click_detect'))
detect_layout.addWidget(self.detected_devices_text)
layout.addWidget(detect_group)
return widget
def create_config_tab(self) -> QWidget:
"""Create the model & firmware configuration tab."""
widget = QWidget()
layout = QVBoxLayout(widget)
# Configuration mode selection
mode_group = QGroupBox("Configuration Mode")
mode_layout = QVBoxLayout(mode_group)
self.folder_mode_radio = QCheckBox(self.t('folder_mode'))
self.folder_mode_radio.setChecked(True)
self.folder_mode_radio.toggled.connect(self.on_config_mode_changed)
mode_layout.addWidget(self.folder_mode_radio)
self.individual_mode_radio = QCheckBox(self.t('individual_mode'))
self.individual_mode_radio.toggled.connect(self.on_config_mode_changed)
mode_layout.addWidget(self.individual_mode_radio)
layout.addWidget(mode_group)
# Folder-based configuration
self.folder_config_group = QGroupBox(self.t('assets_folder'))
folder_config_layout = QFormLayout(self.folder_config_group)
folder_layout = QHBoxLayout()
self.assets_folder_edit = QLineEdit()
self.assets_folder_edit.setPlaceholderText("Select assets folder containing Firmware/ and Models/ subdirectories")
folder_layout.addWidget(self.assets_folder_edit)
self.browse_folder_btn = QPushButton(self.t('browse_folder'))
self.browse_folder_btn.clicked.connect(self.browse_assets_folder)
folder_layout.addWidget(self.browse_folder_btn)
folder_config_layout.addRow(self.t('assets_folder'), folder_layout)
# Create structure button
self.create_structure_btn = QPushButton("Create Assets Structure")
self.create_structure_btn.clicked.connect(self.create_assets_structure)
folder_config_layout.addRow("", self.create_structure_btn)
layout.addWidget(self.folder_config_group)
# Individual file configuration (initially hidden)
self.individual_config_group = QGroupBox(self.t('individual_paths'))
individual_config_layout = QVBoxLayout(self.individual_config_group)
# Scroll area for individual configurations
scroll = QScrollArea()
scroll_content = QWidget()
self.individual_layout = QVBoxLayout(scroll_content)
# Will be populated dynamically based on enabled series
self.individual_configs = {}
scroll.setWidget(scroll_content)
scroll.setWidgetResizable(True)
individual_config_layout.addWidget(scroll)
self.individual_config_group.setVisible(False)
layout.addWidget(self.individual_config_group)
# Validation status
self.config_status_label = QLabel("Select configuration mode and paths")
self.config_status_label.setStyleSheet("color: #6c7086; font-style: italic;")
layout.addWidget(self.config_status_label)
return widget
def create_mapping_tab(self) -> QWidget:
"""Create the port ID mapping tab."""
widget = QWidget()
layout = QVBoxLayout(widget)
# Instructions
instructions = QLabel(
"Map detected devices to specific dongle series. "
"Each port ID should be assigned to exactly one series." if self.language == 'en' else
"將偵測到的裝置映射到特定的加密狗系列。每個連接埠ID應該只指派給一個系列。"
)
instructions.setWordWrap(True)
instructions.setStyleSheet("color: #cdd6f4; background-color: #313244; padding: 10px; border-radius: 5px;")
layout.addWidget(instructions)
# Auto-assign button
auto_assign_layout = QHBoxLayout()
self.auto_assign_btn = QPushButton(self.t('auto_assign'))
self.auto_assign_btn.clicked.connect(self.auto_assign_ports)
auto_assign_layout.addWidget(self.auto_assign_btn)
auto_assign_layout.addStretch()
layout.addLayout(auto_assign_layout)
# Port mapping table
self.port_mapping_table = QTableWidget()
self.port_mapping_table.setColumnCount(3)
self.port_mapping_table.setHorizontalHeaderLabels([
self.t('port_id'), 'Detected Series', self.t('assigned_series')
])
# Make table columns resize properly
header = self.port_mapping_table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
header.setSectionResizeMode(2, QHeaderView.Stretch)
layout.addWidget(self.port_mapping_table)
return widget
def create_validation_tab(self) -> QWidget:
"""Create the configuration validation tab."""
widget = QWidget()
layout = QVBoxLayout(widget)
# Validation controls
validation_controls = QHBoxLayout()
self.validate_btn = QPushButton(self.t('validate_config'))
self.validate_btn.clicked.connect(self.validate_configuration)
validation_controls.addWidget(self.validate_btn)
self.preview_btn = QPushButton(self.t('preview_config'))
self.preview_btn.clicked.connect(self.preview_configuration)
validation_controls.addWidget(self.preview_btn)
validation_controls.addStretch()
layout.addLayout(validation_controls)
# Validation results
self.validation_results = QTextEdit()
self.validation_results.setReadOnly(True)
self.validation_results.setFont(QFont("Consolas", 10))
self.validation_results.setText("Click 'Validate Configuration' to check your settings...")
layout.addWidget(self.validation_results)
return widget
def change_language(self, language_text: str):
"""Change the interface language."""
self.language = 'en' if language_text == 'English' else 'zh'
# Update all UI text
self.setWindowTitle(self.t('title'))
# Update tab titles
self.tab_widget.setTabText(0, self.t('series_selection'))
self.tab_widget.setTabText(1, self.t('model_firmware'))
self.tab_widget.setTabText(2, self.t('port_mapping'))
self.tab_widget.setTabText(3, self.t('validation'))
# Update button text
self.load_config_btn.setText(self.t('load_config'))
self.save_config_btn.setText(self.t('save_config'))
self.help_btn.setText(self.t('help'))
self.ok_btn.setText(self.t('ok'))
self.cancel_btn.setText(self.t('cancel'))
self.detect_devices_btn.setText(self.t('detect_devices'))
self.browse_folder_btn.setText(self.t('browse_folder'))
self.auto_assign_btn.setText(self.t('auto_assign'))
self.validate_btn.setText(self.t('validate_config'))
self.preview_btn.setText(self.t('preview_config'))
# Update table headers
self.series_table.setHorizontalHeaderLabels([
self.t('enable_series'), 'Series', self.t('gops'), self.t('status'), self.t('description')
])
self.port_mapping_table.setHorizontalHeaderLabels([
self.t('port_id'), 'Detected Series', self.t('assigned_series')
])
# Update other UI elements
self.refresh_ui_text()
def refresh_ui_text(self):
"""Refresh translatable UI text."""
# Update series descriptions
series_descriptions = {
"KL520": "Entry-level NPU (3 GOPS)" if self.language == 'en' else "入門級NPU (3 GOPS)",
"KL720": "Mid-range NPU (28 GOPS)" if self.language == 'en' else "中階NPU (28 GOPS)",
"KL630": "High-performance NPU (400 GOPS)" if self.language == 'en' else "高性能NPU (400 GOPS)",
"KL730": "Very high-performance NPU (1600 GOPS)" if self.language == 'en' else "超高性能NPU (1600 GOPS)",
"KL540": "Specialized NPU (800 GOPS)" if self.language == 'en' else "專用NPU (800 GOPS)"
}
for i in range(self.series_table.rowCount()):
series_item = self.series_table.item(i, 1)
if series_item:
series_name = series_item.text()
if series_name in series_descriptions:
self.series_table.setItem(i, 4, QTableWidgetItem(series_descriptions[series_name]))
# Update checkbox text
self.folder_mode_radio.setText(self.t('folder_mode'))
self.individual_mode_radio.setText(self.t('individual_mode'))
def detect_devices(self):
"""Detect available dongle devices."""
try:
# Try to import and use the actual device detection
try:
from core.functions.Multidongle import MultiDongle
devices = MultiDongle.scan_devices()
except ImportError:
# Fallback: simulate some devices for testing
devices = [
{'port_id': 28, 'series': 'KL520', 'product_id': 0x100},
{'port_id': 32, 'series': 'KL720', 'product_id': 0x720}
]
self.detected_devices = devices
# Update detected devices display
if devices:
device_text = f"Found {len(devices)} device(s):\n\n" if self.language == 'en' else f"找到 {len(devices)} 個裝置:\n\n"
for device in devices:
device_text += f"• Port {device['port_id']}: {device['series']}\n"
# Update series table status
series_counts = {}
for device in devices:
series_name = device['series']
series_counts[series_name] = series_counts.get(series_name, 0) + 1
for i in range(self.series_table.rowCount()):
series_item = self.series_table.item(i, 1)
status_item = self.series_table.item(i, 3)
if series_item and status_item:
series_name = series_item.text()
count = series_counts.get(series_name, 0)
if count > 0:
status_text = f"{count} detected" if self.language == 'en' else f"偵測到 {count}"
status_item.setText(status_text)
status_item.setBackground(Qt.green if count > 0 else Qt.red)
else:
status_item.setText("Not detected" if self.language == 'en' else "未偵測")
else:
device_text = "No devices detected. Please check connections." if self.language == 'en' else "未偵測到裝置。請檢查連接。"
self.detected_devices_text.setText(device_text)
# Update port mapping table
self.update_port_mapping_table()
except Exception as e:
error_msg = f"Device detection failed: {e}" if self.language == 'en' else f"裝置偵測失敗:{e}"
self.detected_devices_text.setText(error_msg)
QMessageBox.warning(self, self.t('error'), error_msg)
def update_port_mapping_table(self):
"""Update the port mapping table with detected devices."""
if not self.detected_devices:
self.port_mapping_table.setRowCount(0)
return
self.port_mapping_table.setRowCount(len(self.detected_devices))
for i, device in enumerate(self.detected_devices):
# Port ID
port_item = QTableWidgetItem(str(device['port_id']))
port_item.setFlags(port_item.flags() & ~Qt.ItemIsEditable) # Read-only
self.port_mapping_table.setItem(i, 0, port_item)
# Detected series
detected_item = QTableWidgetItem(device['series'])
detected_item.setFlags(detected_item.flags() & ~Qt.ItemIsEditable) # Read-only
self.port_mapping_table.setItem(i, 1, detected_item)
# Assigned series (combo box)
series_combo = QComboBox()
series_combo.addItem("Unassigned" if self.language == 'en' else "未指派")
# Add available series from the series table
for row in range(self.series_table.rowCount()):
series_item = self.series_table.item(row, 1)
if series_item:
series_combo.addItem(series_item.text())
# Default to detected series if it matches
default_index = series_combo.findText(device['series'])
if default_index > 0:
series_combo.setCurrentIndex(default_index)
self.port_mapping_table.setCellWidget(i, 2, series_combo)
def on_config_mode_changed(self):
"""Handle configuration mode change."""
folder_mode = self.folder_mode_radio.isChecked()
individual_mode = self.individual_mode_radio.isChecked()
# Ensure only one mode is selected
if folder_mode and individual_mode:
if self.sender() == self.folder_mode_radio:
self.individual_mode_radio.setChecked(False)
else:
self.folder_mode_radio.setChecked(False)
# Update visibility
self.folder_config_group.setVisible(self.folder_mode_radio.isChecked())
self.individual_config_group.setVisible(self.individual_mode_radio.isChecked())
# Update individual configs if needed
if self.individual_mode_radio.isChecked():
self.update_individual_configs()
def browse_assets_folder(self):
"""Browse for assets folder."""
current_path = self.assets_folder_edit.text() or os.path.expanduser("~")
folder = QFileDialog.getExistingDirectory(
self,
self.t('select_folder'),
current_path
)
if folder:
self.assets_folder_edit.setText(folder)
self.validate_assets_folder(folder)
def validate_assets_folder(self, folder_path: str):
"""Validate the selected assets folder."""
if not SETUP_UTILS_AVAILABLE:
self.config_status_label.setText("Setup utilities not available")
return
is_valid, issues = MultiSeriesSetup.validate_folder_structure(folder_path)
if is_valid:
self.config_status_label.setText(self.t('config_valid'))
self.config_status_label.setStyleSheet("color: #a6e3a1;")
else:
self.config_status_label.setText(f"{self.t('config_invalid')}: {len(issues)} issues")
self.config_status_label.setStyleSheet("color: #f38ba8;")
def create_assets_structure(self):
"""Create the assets folder structure."""
if not SETUP_UTILS_AVAILABLE:
QMessageBox.warning(self, self.t('warning'), "Setup utilities not available")
return
# Ask user for base path
base_path = QFileDialog.getExistingDirectory(
self,
"Select Base Directory for Assets Folder",
os.path.expanduser("~")
)
if base_path:
# Get enabled series
enabled_series = []
for i in range(self.series_table.rowCount()):
checkbox = self.series_table.cellWidget(i, 0)
series_item = self.series_table.item(i, 1)
if checkbox and checkbox.isChecked() and series_item:
enabled_series.append(series_item.text().replace('KL', ''))
if not enabled_series:
enabled_series = ['520', '720'] # Default series
# Create structure
success = MultiSeriesSetup.create_folder_structure(base_path, enabled_series)
if success:
assets_path = os.path.join(base_path, 'Assets')
self.assets_folder_edit.setText(assets_path)
QMessageBox.information(
self,
self.t('info'),
f"Assets structure created at:\n{assets_path}"
)
else:
QMessageBox.critical(
self,
self.t('error'),
"Failed to create assets structure"
)
def update_individual_configs(self):
"""Update individual file configuration widgets."""
# Clear existing widgets
for i in reversed(range(self.individual_layout.count())):
child = self.individual_layout.itemAt(i).widget()
if child:
child.deleteLater()
self.individual_configs.clear()
# Create config widgets for enabled series
for i in range(self.series_table.rowCount()):
checkbox = self.series_table.cellWidget(i, 0)
series_item = self.series_table.item(i, 1)
if checkbox and checkbox.isChecked() and series_item:
series_name = series_item.text()
config_widget = self.create_individual_config_widget(series_name)
self.individual_layout.addWidget(config_widget)
def create_individual_config_widget(self, series_name: str) -> QWidget:
"""Create individual configuration widget for a series."""
group = QGroupBox(f"{series_name} Configuration")
layout = QFormLayout(group)
# Model path
model_layout = QHBoxLayout()
model_edit = QLineEdit()
model_edit.setPlaceholderText(f"Select {series_name} model file (.nef)")
model_layout.addWidget(model_edit)
model_btn = QPushButton(self.t('browse_file'))
model_btn.clicked.connect(lambda: self.browse_model_file(model_edit, series_name))
model_layout.addWidget(model_btn)
layout.addRow(self.t('model_path'), model_layout)
# SCPU firmware
scpu_layout = QHBoxLayout()
scpu_edit = QLineEdit()
scpu_edit.setPlaceholderText(f"Select {series_name} SCPU firmware (fw_scpu.bin)")
scpu_layout.addWidget(scpu_edit)
scpu_btn = QPushButton(self.t('browse_file'))
scpu_btn.clicked.connect(lambda: self.browse_firmware_file(scpu_edit, series_name, "SCPU"))
scpu_layout.addWidget(scpu_btn)
layout.addRow(self.t('firmware_scpu'), scpu_layout)
# NCPU firmware
ncpu_layout = QHBoxLayout()
ncpu_edit = QLineEdit()
ncpu_edit.setPlaceholderText(f"Select {series_name} NCPU firmware (fw_ncpu.bin)")
ncpu_layout.addWidget(ncpu_edit)
ncpu_btn = QPushButton(self.t('browse_file'))
ncpu_btn.clicked.connect(lambda: self.browse_firmware_file(ncpu_edit, series_name, "NCPU"))
ncpu_layout.addWidget(ncpu_btn)
layout.addRow(self.t('firmware_ncpu'), ncpu_layout)
# Store references
self.individual_configs[series_name] = {
'model': model_edit,
'scpu': scpu_edit,
'ncpu': ncpu_edit
}
return group
def browse_model_file(self, line_edit: QLineEdit, series_name: str):
"""Browse for model file."""
current_path = line_edit.text() or os.path.expanduser("~")
file_path, _ = QFileDialog.getOpenFileName(
self,
f"Select {series_name} Model File",
current_path,
"NEF Model Files (*.nef);;All Files (*)"
)
if file_path:
line_edit.setText(file_path)
def browse_firmware_file(self, line_edit: QLineEdit, series_name: str, fw_type: str):
"""Browse for firmware file."""
current_path = line_edit.text() or os.path.expanduser("~")
file_path, _ = QFileDialog.getOpenFileName(
self,
f"Select {series_name} {fw_type} Firmware",
current_path,
"Binary Files (*.bin);;All Files (*)"
)
if file_path:
line_edit.setText(file_path)
def auto_assign_ports(self):
"""Automatically assign port IDs to matching series."""
for i in range(self.port_mapping_table.rowCount()):
detected_item = self.port_mapping_table.item(i, 1)
assigned_combo = self.port_mapping_table.cellWidget(i, 2)
if detected_item and assigned_combo:
detected_series = detected_item.text()
# Find matching series in combo
index = assigned_combo.findText(detected_series)
if index > 0: # Skip "Unassigned" at index 0
assigned_combo.setCurrentIndex(index)
def validate_configuration(self):
"""Validate the current configuration."""
config = self.get_configuration()
issues = []
# Check if any series are enabled
if not config['enabled_series']:
issues.append("No series selected")
# Validate paths based on mode
if config['config_mode'] == 'folder':
assets_folder = config.get('assets_folder', '')
if not assets_folder:
issues.append("Assets folder not specified")
elif not os.path.exists(assets_folder):
issues.append("Assets folder does not exist")
elif SETUP_UTILS_AVAILABLE:
is_valid, folder_issues = MultiSeriesSetup.validate_folder_structure(assets_folder)
if not is_valid:
issues.extend(folder_issues)
elif config['config_mode'] == 'individual':
individual_paths = config.get('individual_paths', {})
for series in config['enabled_series']:
if series not in individual_paths:
issues.append(f"{series}: Configuration missing")
else:
series_config = individual_paths[series]
for path_type, path in series_config.items():
if not path:
issues.append(f"{series}: {path_type} path not specified")
elif not os.path.exists(path):
issues.append(f"{series}: {path_type} file does not exist")
# Validate port mapping
port_mapping = config.get('port_mapping', {})
if not port_mapping:
issues.append("No port mappings configured")
else:
assigned_ports = set()
for port_id, assigned_series in port_mapping.items():
if assigned_series in assigned_ports:
issues.append(f"Series {assigned_series} assigned to multiple ports")
assigned_ports.add(assigned_series)
# Display results
if not issues:
result_text = f"{self.t('config_valid')}\n\n"
result_text += f"Enabled series: {', '.join(config['enabled_series'])}\n"
result_text += f"Configuration mode: {config['config_mode']}\n"
result_text += f"Port mappings: {len(port_mapping)}\n"
else:
result_text = f"{self.t('config_invalid')}\n\n"
result_text += f"Issues found ({len(issues)}):\n"
for i, issue in enumerate(issues, 1):
result_text += f"{i}. {issue}\n"
self.validation_results.setText(result_text)
def preview_configuration(self):
"""Preview the current configuration."""
config = self.get_configuration()
# Format configuration for display
preview_text = "=== Multi-Series Configuration Preview ===\n\n"
preview_text += json.dumps(config, indent=2, ensure_ascii=False)
self.validation_results.setText(preview_text)
def load_configuration_from_file(self):
"""Load configuration from a JSON file."""
file_path, _ = QFileDialog.getOpenFileName(
self,
"Load Configuration",
os.path.expanduser("~"),
"JSON Files (*.json);;All Files (*)"
)
if file_path:
try:
with open(file_path, 'r', encoding='utf-8') as f:
config = json.load(f)
self.load_configuration(config)
QMessageBox.information(self, self.t('info'), "Configuration loaded successfully")
except Exception as e:
QMessageBox.critical(self, self.t('error'), f"Failed to load configuration: {e}")
def save_configuration_to_file(self):
"""Save current configuration to a JSON file."""
config = self.get_configuration()
file_path, _ = QFileDialog.getSaveFileName(
self,
"Save Configuration",
os.path.expanduser("~/multi_series_config.json"),
"JSON Files (*.json);;All Files (*)"
)
if file_path:
try:
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
QMessageBox.information(self, self.t('info'), "Configuration saved successfully")
except Exception as e:
QMessageBox.critical(self, self.t('error'), f"Failed to save configuration: {e}")
def load_configuration(self, config: Dict[str, Any]):
"""Load configuration into the UI."""
self.current_config = config
# Load language
self.language = config.get('language', 'en')
self.language_combo.setCurrentText("English")
# Load enabled series
enabled_series = config.get('enabled_series', [])
for i in range(self.series_table.rowCount()):
checkbox = self.series_table.cellWidget(i, 0)
series_item = self.series_table.item(i, 1)
if checkbox and series_item:
series_name = series_item.text()
checkbox.setChecked(series_name in enabled_series)
# Load configuration mode
config_mode = config.get('config_mode', 'folder')
if config_mode == 'folder':
self.folder_mode_radio.setChecked(True)
self.individual_mode_radio.setChecked(False)
self.assets_folder_edit.setText(config.get('assets_folder', ''))
else:
self.folder_mode_radio.setChecked(False)
self.individual_mode_radio.setChecked(True)
# Load individual paths would need to be implemented
self.on_config_mode_changed()
def load_current_config(self):
"""Load the current configuration passed to the dialog."""
if self.current_config:
self.load_configuration(self.current_config)
def get_configuration(self) -> Dict[str, Any]:
"""Get the current configuration from the UI."""
config = {
'language': self.language,
'enabled_series': [],
'config_mode': 'folder' if self.folder_mode_radio.isChecked() else 'individual',
'port_mapping': {},
'detected_devices': self._serialize_detected_devices()
}
# Get enabled series
for i in range(self.series_table.rowCount()):
checkbox = self.series_table.cellWidget(i, 0)
series_item = self.series_table.item(i, 1)
if checkbox and checkbox.isChecked() and series_item:
config['enabled_series'].append(series_item.text())
# Get configuration paths
if config['config_mode'] == 'folder':
config['assets_folder'] = self.assets_folder_edit.text()
else:
config['individual_paths'] = {}
for series_name, widgets in self.individual_configs.items():
config['individual_paths'][series_name] = {
'model': widgets['model'].text(),
'scpu': widgets['scpu'].text(),
'ncpu': widgets['ncpu'].text()
}
# Get port mapping
for i in range(self.port_mapping_table.rowCount()):
port_item = self.port_mapping_table.item(i, 0)
assigned_combo = self.port_mapping_table.cellWidget(i, 2)
if port_item and assigned_combo:
port_id = int(port_item.text())
assigned_series = assigned_combo.currentText()
if assigned_series and assigned_series != "Unassigned" and assigned_series != "未指派":
config['port_mapping'][port_id] = assigned_series
return config
def _serialize_detected_devices(self) -> List[Dict[str, Any]]:
"""Serialize detected devices for JSON compatibility."""
serialized_devices = []
for device in self.detected_devices:
serialized_device = {
'port_id': device.get('port_id', 0),
'series': device.get('series', 'Unknown')
}
# If device_descriptor exists, extract serializable information
if 'device_descriptor' in device:
desc = device['device_descriptor']
if hasattr(desc, 'product_id'):
serialized_device['product_id'] = desc.product_id
if hasattr(desc, 'usb_port_id'):
serialized_device['usb_port_id'] = desc.usb_port_id
serialized_devices.append(serialized_device)
return serialized_devices
def show_help(self):
"""Show help dialog."""
help_text = """
Multi-Series Configuration Help
This dialog helps you configure multiple dongle series for improved inference performance.
Configuration Steps:
1. Series Selection: Enable the dongle series you want to use
2. Model & Firmware: Choose folder structure or individual files
3. Port Mapping: Assign detected devices to specific series
4. Validation: Verify your configuration is correct
Folder Structure (Recommended):
- Assets/
- Firmware/KL520/, KL720/, etc.
- Models/KL520/, KL720/, etc.
Individual Files:
- Specify model and firmware files separately for each series
Port Mapping:
- Each detected device should be assigned to exactly one series
- Use "Auto Assign" to automatically match detected series
Tips:
- Use "Create Assets Structure" to create the recommended folder layout
- Validate configuration before saving
- Save/load configurations for different setups
""" if self.language == 'en' else """
多系列配置說明
此對話框幫助您配置多個加密狗系列以提升推理性能。
配置步驟:
1. 系列選擇:啟用您要使用的加密狗系列
2. 模型與韌體:選擇資料夾結構或個別檔案
3. 連接埠映射:將偵測到的裝置指派給特定系列
4. 驗證:確認您的配置正確
資料夾結構(推薦):
- Assets/
- Firmware/KL520/, KL720/, 等
- Models/KL520/, KL720/, 等
個別檔案:
- 為每個系列分別指定模型和韌體檔案
連接埠映射:
- 每個偵測到的裝置應該只指派給一個系列
- 使用「自動指派」自動匹配偵測到的系列
提示:
- 使用「建立資源結構」建立推薦的資料夾配置
- 儲存前驗證配置
- 儲存/載入不同設定的配置
"""
QMessageBox.information(self, self.t('help'), help_text.strip())
def apply_theme(self):
"""Apply consistent theme to the dialog."""
self.setStyleSheet("""
QDialog {
background-color: #1e1e2e;
color: #cdd6f4;
}
QTabWidget::pane {
border: 1px solid #45475a;
background-color: #313244;
}
QTabWidget::tab-bar {
alignment: center;
}
QTabBar::tab {
background-color: #45475a;
color: #cdd6f4;
padding: 8px 16px;
margin-right: 2px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
QTabBar::tab:selected {
background-color: #89b4fa;
color: #1e1e2e;
}
QTabBar::tab:hover {
background-color: #585b70;
}
QGroupBox {
font-weight: bold;
border: 2px solid #45475a;
border-radius: 5px;
margin-top: 1ex;
padding-top: 5px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 10px 0 10px;
}
QPushButton {
background-color: #45475a;
color: #cdd6f4;
border: 1px solid #6c7086;
border-radius: 4px;
padding: 8px 16px;
font-weight: bold;
}
QPushButton:hover {
background-color: #585b70;
}
QPushButton:pressed {
background-color: #313244;
}
QPushButton:disabled {
background-color: #313244;
color: #6c7086;
}
QTextEdit, QLineEdit {
background-color: #313244;
color: #cdd6f4;
border: 1px solid #45475a;
border-radius: 4px;
padding: 4px;
}
QTableWidget {
background-color: #313244;
alternate-background-color: #45475a;
color: #cdd6f4;
border: 1px solid #45475a;
}
QComboBox {
background-color: #313244;
color: #cdd6f4;
border: 1px solid #45475a;
border-radius: 4px;
padding: 4px;
}
QComboBox::drop-down {
border: none;
}
QComboBox::down-arrow {
image: url(down_arrow.png);
width: 12px;
height: 12px;
}
QCheckBox {
color: #cdd6f4;
}
QCheckBox::indicator {
width: 15px;
height: 15px;
}
QCheckBox::indicator:unchecked {
background-color: #313244;
border: 2px solid #45475a;
border-radius: 3px;
}
QCheckBox::indicator:checked {
background-color: #89b4fa;
border: 2px solid #89b4fa;
border-radius: 3px;
}
""")
def set_detected_devices(self, devices: List[Dict[str, Any]]):
"""Set detected devices from parent window."""
self.detected_devices = devices
self.update_port_mapping_table()
# Update detected devices display
if devices:
device_text = f"Found {len(devices)} device(s):\n\n" if self.language == 'en' else f"找到 {len(devices)} 個裝置:\n\n"
for device in devices:
device_text += f"• Port {device['port_id']}: {device['series']}\n"
else:
device_text = "No devices detected. Please check connections." if self.language == 'en' else "未偵測到裝置。請檢查連接。"
self.detected_devices_text.setText(device_text)