1207 lines
48 KiB
Python
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) |