Fix: Add timeout mechanisms to prevent blocking on device scan and camera open

- device_service: Add timeout mechanism for device scanning
- video_thread: Add timeout mechanism for camera opening
- device_list: Fix height and hide scrollbars to prevent scroll issues
- mainWindows: Adjust UI startup order, delay device refresh and camera start
- utilities_screen: Temporarily disable auto refresh to prevent blocking
- .gitignore: Add new ignore entries

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
HuangMason320 2025-12-27 02:37:46 +08:00
parent 0c33dd059f
commit c8be1db25e
7 changed files with 142 additions and 40 deletions

3
.gitignore vendored
View File

@ -42,3 +42,6 @@ dist/output/
dist/main.exe dist/main.exe
*.whl *.whl
win_driver/ win_driver/
claude.md
src/services/__pycache__/device_service.cpython-312.pyc
src/__pycache__/config.cpython-312.pyc

View File

@ -1,15 +1,53 @@
import cv2 import cv2
import time
import threading
from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtGui import QImage from PyQt5.QtGui import QImage
class VideoThread(QThread): class VideoThread(QThread):
change_pixmap_signal = pyqtSignal(QImage) change_pixmap_signal = pyqtSignal(QImage)
camera_error_signal = pyqtSignal(str) # 新增:相機錯誤信號
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._run_flag = True self._run_flag = True
self._camera_open_attempts = 0 self._camera_open_attempts = 0
self._max_attempts = 3 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): def run(self):
# 嘗試多次開啟相機 # 嘗試多次開啟相機
@ -18,13 +56,12 @@ class VideoThread(QThread):
print(f"嘗試開啟相機 (嘗試 {self._camera_open_attempts}/{self._max_attempts})...") print(f"嘗試開啟相機 (嘗試 {self._camera_open_attempts}/{self._max_attempts})...")
# 嘗試使用DirectShow後端通常在Windows上更快 # 嘗試使用DirectShow後端通常在Windows上更快
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW) cap = self._open_camera_with_timeout(0, cv2.CAP_DSHOW)
if not cap.isOpened(): if cap is None:
print("無法使用DirectShow開啟相機嘗試預設後端") print("無法使用DirectShow開啟相機嘗試預設後端")
cap = cv2.VideoCapture(0) cap = self._open_camera_with_timeout(0)
if not cap.isOpened(): if cap is None:
print(f"無法使用任何後端開啟相機等待1秒後重試...") print(f"無法使用任何後端開啟相機等待1秒後重試...")
import time
time.sleep(1) time.sleep(1)
continue continue

View File

@ -1,18 +1,63 @@
import kp import kp
import threading
def check_available_device():
try:
print("checking available devices")
device_descriptors = kp.core.scan_devices()
print("device_descriptors", device_descriptors)
return device_descriptors
except Exception as e:
print(f"Error scanning devices: {e}")
# 返回一個空的設備描述符或模擬數據
class EmptyDescriptor: class EmptyDescriptor:
def __init__(self): def __init__(self):
self.device_descriptor_number = 0 self.device_descriptor_number = 0
self.device_descriptor_list = [] 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
try:
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"[SCAN] Error scanning devices: {e}")
return EmptyDescriptor() return EmptyDescriptor()
# def check_available_device(): # def check_available_device():

View File

@ -12,10 +12,8 @@ def create_device_layout(parent, device_controller):
devices_frame = QFrame(parent) devices_frame = QFrame(parent)
devices_frame.setStyleSheet(f"border: none; background: {SECONDARY_COLOR}; border-radius: 15px;") devices_frame.setStyleSheet(f"border: none; background: {SECONDARY_COLOR}; border-radius: 15px;")
# Set height based on connected devices # 固定高度,避免滾輪問題
base_height = 250 devices_frame.setFixedHeight(200)
extra_height = 100 if len(device_controller.connected_devices) > 1 else 0
devices_frame.setFixedHeight(base_height + extra_height)
devices_frame.setFixedWidth(240) devices_frame.setFixedWidth(240)
devices_layout = QVBoxLayout(devices_frame) devices_layout = QVBoxLayout(devices_frame)
@ -53,7 +51,15 @@ def create_device_layout(parent, device_controller):
QListWidget::item:selected { QListWidget::item:selected {
background-color: rgba(255, 255, 255, 0.2); 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 # Connect item selection signal
device_list_widget.itemClicked.connect(lambda item: device_controller.select_device( device_list_widget.itemClicked.connect(lambda item: device_controller.select_device(

View File

@ -207,8 +207,10 @@ def create_device_popup(parent, device_controller):
def refresh_devices(parent, device_controller): def refresh_devices(parent, device_controller):
"""Refresh the device list and update the UI""" """Refresh the device list and update the UI"""
try: try:
print("[POPUP] 開始刷新設備列表...")
# Call the refresh method from device controller # Call the refresh method from device controller
device_controller.refresh_devices() device_controller.refresh_devices()
print("[POPUP] device_controller.refresh_devices() 完成")
# Clear the list # Clear the list
parent.device_list_widget_popup.clear() parent.device_list_widget_popup.clear()

View File

@ -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.toolbox import create_ai_toolbox
from src.views.components.media_panel import create_media_panel 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.device_popup import create_device_popup, refresh_devices
from src.views.components.custom_model_block import create_custom_model_block
class MainWindow(QWidget): class MainWindow(QWidget):
def __init__(self): def __init__(self):
@ -92,10 +93,11 @@ class MainWindow(QWidget):
# 3. Refresh devices - do this after UI is fully set up # 3. Refresh devices - do this after UI is fully set up
QTimer.singleShot(100, self.device_controller.refresh_devices) QTimer.singleShot(100, self.device_controller.refresh_devices)
self.auto_start_camera()
# # 4. Show popup # # 4. Show popup
self.show_device_popup() self.show_device_popup()
# 5. 延遲啟動相機,讓 UI 先完全顯示
QTimer.singleShot(500, self.auto_start_camera)
print("Popup window setup complete") print("Popup window setup complete")
# # 5. Start camera automatically after a short delay # # 5. Start camera automatically after a short delay
@ -109,9 +111,13 @@ class MainWindow(QWidget):
main_layout = QHBoxLayout(main_page) main_layout = QHBoxLayout(main_page)
main_page.setLayout(main_layout) main_page.setLayout(main_layout)
# Left layout # Left layout - 使用固定寬度的容器,避免滾輪
left_layout = QVBoxLayout() left_container = QWidget()
main_layout.addLayout(left_layout, 1) 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 # Add Kneron logo
logo_label = QLabel() logo_label = QLabel()
@ -121,12 +127,16 @@ class MainWindow(QWidget):
logo_label.setPixmap(scaled_logo) logo_label.setPixmap(scaled_logo)
left_layout.addWidget(logo_label) 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) self.device_frame, self.device_list_widget = create_device_layout(self, self.device_controller)
left_layout.addWidget(self.device_frame) left_layout.addWidget(self.device_frame)
self.toolbox_frame = create_ai_toolbox(self, self.config_utils, self.inference_controller) # 使用 Custom Model Block 取代原本的 AI Toolbox
left_layout.addWidget(self.toolbox_frame) 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 layout
right_container = QWidget() right_container = QWidget()
@ -175,13 +185,12 @@ class MainWindow(QWidget):
def show_device_popup(self): def show_device_popup(self):
try: try:
# 顯示彈窗前刷新設備列表 # 先顯示彈窗
# 使用新的 refresh_devices 函數,而不是直接調用 device_controller
from src.views.components.device_popup import refresh_devices
refresh_devices(self, self.device_controller)
# 顯示彈窗
self.overlay.show() 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: except Exception as e:
print(f"Error in show_device_popup: {e}") print(f"Error in show_device_popup: {e}")

View File

@ -68,8 +68,8 @@ class UtilitiesScreen(QWidget):
# 添加內容容器到主佈局 # 添加內容容器到主佈局
self.main_layout.addWidget(self.content_container, 1) self.main_layout.addWidget(self.content_container, 1)
# Initialize with device refresh # Initialize with device refresh (暫時禁用自動刷新,避免阻塞)
QTimer.singleShot(500, self.refresh_devices) # QTimer.singleShot(500, self.refresh_devices)
def create_header(self): def create_header(self):
"""Create the header with back button and logo""" """Create the header with back button and logo"""