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:
parent
0c33dd059f
commit
c8be1db25e
5
.gitignore
vendored
5
.gitignore
vendored
@ -41,4 +41,7 @@ main.spec
|
||||
dist/output/
|
||||
dist/main.exe
|
||||
*.whl
|
||||
win_driver/
|
||||
win_driver/
|
||||
claude.md
|
||||
src/services/__pycache__/device_service.cpython-312.pyc
|
||||
src/__pycache__/config.cpython-312.pyc
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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}")
|
||||
|
||||
|
||||
@ -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"""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user