""" 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)