diff --git a/.gitignore b/.gitignore index 660c7b1..d98d406 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,7 @@ main.spec dist/output/ dist/main.exe *.whl -win_driver/ \ No newline at end of file +win_driver/ +claude.md +src/services/__pycache__/device_service.cpython-312.pyc +src/__pycache__/config.cpython-312.pyc diff --git a/src/models/video_thread.py b/src/models/video_thread.py index 3b8ec2f..77ddd5a 100644 --- a/src/models/video_thread.py +++ b/src/models/video_thread.py @@ -1,30 +1,67 @@ import cv2 +import time +import threading from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtGui import QImage class VideoThread(QThread): change_pixmap_signal = pyqtSignal(QImage) + camera_error_signal = pyqtSignal(str) # 新增:相機錯誤信號 def __init__(self): super().__init__() self._run_flag = True self._camera_open_attempts = 0 self._max_attempts = 3 + self._camera_timeout = 5 # 相機開啟超時秒數 + + def _open_camera_with_timeout(self, camera_index, backend=None): + """使用超時機制開啟相機""" + cap = None + open_success = False + + def try_open(): + nonlocal cap, open_success + try: + if backend is not None: + cap = cv2.VideoCapture(camera_index, backend) + else: + cap = cv2.VideoCapture(camera_index) + open_success = cap is not None and cap.isOpened() + except Exception as e: + print(f"開啟相機時發生異常: {e}") + open_success = False + + # 在單獨的線程中嘗試開啟相機 + thread = threading.Thread(target=try_open) + thread.daemon = True + thread.start() + thread.join(timeout=self._camera_timeout) + + if thread.is_alive(): + print(f"相機開啟超時 ({self._camera_timeout}秒)") + return None + + if open_success: + return cap + else: + if cap is not None: + cap.release() + return None def run(self): # 嘗試多次開啟相機 while self._camera_open_attempts < self._max_attempts and self._run_flag: self._camera_open_attempts += 1 print(f"嘗試開啟相機 (嘗試 {self._camera_open_attempts}/{self._max_attempts})...") - + # 嘗試使用DirectShow後端,通常在Windows上更快 - cap = cv2.VideoCapture(0, cv2.CAP_DSHOW) - if not cap.isOpened(): + cap = self._open_camera_with_timeout(0, cv2.CAP_DSHOW) + if cap is None: print("無法使用DirectShow開啟相機,嘗試預設後端") - cap = cv2.VideoCapture(0) - if not cap.isOpened(): + cap = self._open_camera_with_timeout(0) + if cap is None: print(f"無法使用任何後端開啟相機,等待1秒後重試...") - import time time.sleep(1) continue diff --git a/src/services/device_service.py b/src/services/device_service.py index ed0cedc..bccb90e 100644 --- a/src/services/device_service.py +++ b/src/services/device_service.py @@ -1,18 +1,63 @@ import kp +import threading + +class EmptyDescriptor: + def __init__(self): + self.device_descriptor_number = 0 + self.device_descriptor_list = [] + +def check_available_device(timeout=0.5): + """ + 掃描可用設備,帶有超時機制 + + Args: + timeout: 超時秒數,預設 5 秒 + + Returns: + 設備描述符 + """ + result = [None] + error = [None] + + def scan_devices(): + try: + result[0] = kp.core.scan_devices() + except Exception as e: + error[0] = e -def check_available_device(): try: - print("checking available devices") - device_descriptors = kp.core.scan_devices() - print("device_descriptors", device_descriptors) - return device_descriptors + print("[SCAN] 開始掃描設備...") + + # 在單獨的線程中執行掃描 + thread = threading.Thread(target=scan_devices) + thread.daemon = True + print("[SCAN] 啟動掃描線程...") + thread.start() + print(f"[SCAN] 等待掃描完成 (超時: {timeout}秒)...") + thread.join(timeout=timeout) + + if thread.is_alive(): + print(f"[SCAN] 設備掃描超時 ({timeout}秒)") + return EmptyDescriptor() + + print("[SCAN] 掃描線程已完成") + + if error[0]: + print(f"[SCAN] Error scanning devices: {error[0]}") + return EmptyDescriptor() + + if result[0] is None: + print("[SCAN] 結果為 None") + return EmptyDescriptor() + + print("[SCAN] device_descriptors:", result[0]) + print("[SCAN] 準備返回結果...") + import sys + sys.stdout.flush() + return result[0] + except Exception as e: - print(f"Error scanning devices: {e}") - # 返回一個空的設備描述符或模擬數據 - class EmptyDescriptor: - def __init__(self): - self.device_descriptor_number = 0 - self.device_descriptor_list = [] + print(f"[SCAN] Error scanning devices: {e}") return EmptyDescriptor() # def check_available_device(): diff --git a/src/views/components/device_list.py b/src/views/components/device_list.py index 1fb08fd..4812ee6 100644 --- a/src/views/components/device_list.py +++ b/src/views/components/device_list.py @@ -11,11 +11,9 @@ def create_device_layout(parent, device_controller): try: devices_frame = QFrame(parent) devices_frame.setStyleSheet(f"border: none; background: {SECONDARY_COLOR}; border-radius: 15px;") - - # Set height based on connected devices - base_height = 250 - extra_height = 100 if len(device_controller.connected_devices) > 1 else 0 - devices_frame.setFixedHeight(base_height + extra_height) + + # 固定高度,避免滾輪問題 + devices_frame.setFixedHeight(200) devices_frame.setFixedWidth(240) devices_layout = QVBoxLayout(devices_frame) @@ -53,7 +51,15 @@ def create_device_layout(parent, device_controller): QListWidget::item:selected { background-color: rgba(255, 255, 255, 0.2); } + QScrollBar:vertical { + width: 0px; + } + QScrollBar:horizontal { + height: 0px; + } """) + device_list_widget.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + device_list_widget.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # Connect item selection signal device_list_widget.itemClicked.connect(lambda item: device_controller.select_device( diff --git a/src/views/components/device_popup.py b/src/views/components/device_popup.py index b9ba9bb..ef6cd98 100644 --- a/src/views/components/device_popup.py +++ b/src/views/components/device_popup.py @@ -207,8 +207,10 @@ def create_device_popup(parent, device_controller): def refresh_devices(parent, device_controller): """Refresh the device list and update the UI""" try: + print("[POPUP] 開始刷新設備列表...") # Call the refresh method from device controller device_controller.refresh_devices() + print("[POPUP] device_controller.refresh_devices() 完成") # Clear the list parent.device_list_widget_popup.clear() diff --git a/src/views/mainWindows.py b/src/views/mainWindows.py index dc586b0..35f1494 100644 --- a/src/views/mainWindows.py +++ b/src/views/mainWindows.py @@ -14,6 +14,7 @@ from src.views.components.device_list import create_device_layout from src.views.components.toolbox import create_ai_toolbox from src.views.components.media_panel import create_media_panel from src.views.components.device_popup import create_device_popup, refresh_devices +from src.views.components.custom_model_block import create_custom_model_block class MainWindow(QWidget): def __init__(self): @@ -92,10 +93,11 @@ class MainWindow(QWidget): # 3. Refresh devices - do this after UI is fully set up QTimer.singleShot(100, self.device_controller.refresh_devices) - self.auto_start_camera() - # # 4. Show popup self.show_device_popup() + + # 5. 延遲啟動相機,讓 UI 先完全顯示 + QTimer.singleShot(500, self.auto_start_camera) print("Popup window setup complete") # # 5. Start camera automatically after a short delay @@ -109,9 +111,13 @@ class MainWindow(QWidget): main_layout = QHBoxLayout(main_page) main_page.setLayout(main_layout) - # Left layout - left_layout = QVBoxLayout() - main_layout.addLayout(left_layout, 1) + # Left layout - 使用固定寬度的容器,避免滾輪 + left_container = QWidget() + left_container.setFixedWidth(260) + left_layout = QVBoxLayout(left_container) + left_layout.setContentsMargins(10, 10, 10, 10) + left_layout.setSpacing(10) + main_layout.addWidget(left_container) # Add Kneron logo logo_label = QLabel() @@ -121,12 +127,16 @@ class MainWindow(QWidget): logo_label.setPixmap(scaled_logo) left_layout.addWidget(logo_label) - # Add device list and AI toolbox + # Add device list and custom model block self.device_frame, self.device_list_widget = create_device_layout(self, self.device_controller) left_layout.addWidget(self.device_frame) - - self.toolbox_frame = create_ai_toolbox(self, self.config_utils, self.inference_controller) - left_layout.addWidget(self.toolbox_frame) + + # 使用 Custom Model Block 取代原本的 AI Toolbox + self.custom_model_frame = create_custom_model_block(self, self.inference_controller) + left_layout.addWidget(self.custom_model_frame) + + # 添加彈性空間,確保元件不會被拉伸 + left_layout.addStretch() # Right layout right_container = QWidget() @@ -175,13 +185,12 @@ class MainWindow(QWidget): def show_device_popup(self): try: - # 顯示彈窗前刷新設備列表 - # 使用新的 refresh_devices 函數,而不是直接調用 device_controller - from src.views.components.device_popup import refresh_devices - refresh_devices(self, self.device_controller) - - # 顯示彈窗 + # 先顯示彈窗 self.overlay.show() + + # 延遲刷新設備列表,讓 UI 先顯示 + from src.views.components.device_popup import refresh_devices + QTimer.singleShot(100, lambda: refresh_devices(self, self.device_controller)) except Exception as e: print(f"Error in show_device_popup: {e}") diff --git a/src/views/utilities_screen.py b/src/views/utilities_screen.py index f03a502..2a79ace 100644 --- a/src/views/utilities_screen.py +++ b/src/views/utilities_screen.py @@ -68,8 +68,8 @@ class UtilitiesScreen(QWidget): # 添加內容容器到主佈局 self.main_layout.addWidget(self.content_container, 1) - # Initialize with device refresh - QTimer.singleShot(500, self.refresh_devices) + # Initialize with device refresh (暫時禁用自動刷新,避免阻塞) + # QTimer.singleShot(500, self.refresh_devices) def create_header(self): """Create the header with back button and logo"""