diff --git a/src/config.py b/src/config.py index c020fc6..e659a0c 100644 --- a/src/config.py +++ b/src/config.py @@ -1,7 +1,7 @@ from enum import Enum import os -# APPDATA_PATH = os.environ.get("LOCALAPPDATA") -APPDATA_PATH = "/Users/mason/Developer/Kneron-Academy/test_images" +APPDATA_PATH = os.environ.get("LOCALAPPDATA") +# APPDATA_PATH = "/Users/mason/Developer/Kneron-Academy/test_images" # 取得專案根目錄的絕對路徑並設定 UXUI_ASSETS 為絕對路徑 PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) UXUI_ASSETS = os.path.join(PROJECT_ROOT, "uxui", "") diff --git a/src/controllers/device_controller.py b/src/controllers/device_controller.py index 0ba6611..dc54f7b 100644 --- a/src/controllers/device_controller.py +++ b/src/controllers/device_controller.py @@ -1,6 +1,6 @@ # src/controllers/device_controller.py from PyQt5.QtWidgets import QWidget, QListWidgetItem -from PyQt5.QtGui import QPixmap +from PyQt5.QtGui import QPixmap, QIcon from PyQt5.QtCore import Qt import os @@ -19,6 +19,7 @@ class DeviceController: print("Refreshing devices...") device_descriptors = check_available_device() self.connected_devices = [] + # print(self.connected_devices) if device_descriptors.device_descriptor_number > 0: self.parse_and_store_devices(device_descriptors.device_descriptor_list) @@ -34,27 +35,79 @@ class DeviceController: def parse_and_store_devices(self, devices): """Parse device information and store it""" for device in devices: - product_id = hex(device.product_id).strip().lower() - dongle = DongleModelMap.get(product_id, "unknown") - device.dongle = dongle + try: + product_id = hex(device.product_id).strip().lower() + dongle = DongleModelMap.get(product_id, "unknown") + device.dongle = dongle - new_device = { - 'usb_port_id': device.usb_port_id, - 'product_id': device.product_id, - 'kn_number': device.kn_number, - 'dongle': dongle - } + new_device = { + 'usb_port_id': device.usb_port_id, + 'product_id': device.product_id, + 'kn_number': device.kn_number, + 'dongle': dongle + } + + existing_device_index = next( + (index for (index, d) in enumerate(self.connected_devices) + if d['usb_port_id'] == new_device['usb_port_id']), + None + ) + + if existing_device_index is not None: + self.connected_devices[existing_device_index] = new_device + else: + self.connected_devices.append(new_device) + except Exception as e: + print(f"Error processing device: {e}") + + def display_devices(self, devices): + """Display the connected devices in the UI""" + try: + if not hasattr(self.main_window, 'device_list_widget'): + print("Warning: main_window does not have device_list_widget attribute") + return + + self.main_window.device_list_widget.clear() - existing_device_index = next( - (index for (index, d) in enumerate(self.connected_devices) - if d['usb_port_id'] == new_device['usb_port_id']), - None - ) - - if existing_device_index is not None: - self.connected_devices[existing_device_index] = new_device - else: - self.connected_devices.append(new_device) + if not devices: + print("No devices to display") + return + + for device in devices: + try: + product_id = hex(device.product_id).strip().lower() + icon_path = os.path.join(UXUI_ASSETS, DongleIconMap.get(product_id, "unknown_dongle.png")) + + item = QListWidgetItem() + item.setData(Qt.UserRole, device) + + pixmap = QPixmap(icon_path) + icon = QIcon(pixmap) # Convert QPixmap to QIcon + item.setIcon(icon) + + # Set device name as the display text + dongle_name = DongleModelMap.get(product_id, "Unknown Device") + item.setText(f"{dongle_name} (KN: {device.kn_number})") + + self.main_window.device_list_widget.addItem(item) + print(f"Added device to list: {dongle_name} (KN: {device.kn_number})") + except Exception as e: + print(f"Error adding device to list: {e}") + except Exception as e: + print(f"Error in display_devices: {e}") + + def get_devices(self): + """Get the list of connected devices""" + try: + device_descriptors = check_available_device() + if device_descriptors.device_descriptor_number > 0: + # Parse and store devices to ensure connected_devices is updated + self.parse_and_store_devices(device_descriptors.device_descriptor_list) + return device_descriptors.device_descriptor_list + return [] + except Exception as e: + print(f"Error in get_devices: {e}") + return [] def get_selected_device(self): """Get the currently selected device""" @@ -69,7 +122,9 @@ class DeviceController: for index in range(list_widget.count()): item = list_widget.item(index) widget = list_widget.itemWidget(item) - widget.setStyleSheet("background: none;") + if widget: # Check if widget exists before setting style + widget.setStyleSheet("background: none;") list_item_widget = list_widget.itemWidget(list_item) - list_item_widget.setStyleSheet("background-color: lightblue;") \ No newline at end of file + if list_item_widget: # Check if widget exists before setting style + list_item_widget.setStyleSheet("background-color: lightblue;") \ No newline at end of file diff --git a/src/controllers/inference_controller.py b/src/controllers/inference_controller.py index c5c3a7d..040603c 100644 --- a/src/controllers/inference_controller.py +++ b/src/controllers/inference_controller.py @@ -1,9 +1,10 @@ # src/controllers/inference_controller.py import os, queue, cv2, json -from PyQt5.QtWidgets import QMessageBox +from PyQt5.QtWidgets import QMessageBox, QApplication +from PyQt5.QtCore import QTimer, Qt from src.models.inference_worker import InferenceWorkerThread -from src.config import UTILS_DIR, FW_DIR +from src.config import UTILS_DIR, FW_DIR, DongleModelMap class InferenceController: def __init__(self, main_window, device_controller): @@ -12,128 +13,248 @@ class InferenceController: self.inference_worker = None self.inference_queue = queue.Queue(maxsize=10) self.current_tool_config = None + self.previous_tool_config = None + self._camera_was_active = False + # 儲存原始影格尺寸,用於邊界框縮放計算 + self.original_frame_width = 640 # 預設值 + self.original_frame_height = 480 # 預設值 def select_tool(self, tool_config): - """Select an AI tool and configure inference""" - print("Selected tool:", tool_config.get("display_name")) - self.current_tool_config = tool_config - - # Get mode and model name - mode = tool_config.get("mode", "") - model_name = tool_config.get("model_name", "") - - # Load detailed model configuration - model_path = os.path.join(UTILS_DIR, mode, model_name) - model_config_path = os.path.join(model_path, "config.json") - - if os.path.exists(model_config_path): - try: - with open(model_config_path, "r", encoding="utf-8") as f: - detailed_config = json.load(f) - tool_config = {**tool_config, **detailed_config} - except Exception as e: - print(f"Error reading model config: {e}") - - # Get tool input type - input_info = tool_config.get("input_info", {}) - tool_type = input_info.get("type", "video") - once_mode = True if tool_type == "image" else False - - # Prepare input parameters - input_params = tool_config.get("input_parameters", {}).copy() - - # Configure device-related settings - selected_device = self.device_controller.get_selected_device() - if selected_device: - input_params["usb_port_id"] = selected_device.get("usb_port_id", 0) - dongle = selected_device.get("dongle", "unknown") + """選擇AI工具並配置推論""" + try: + print("選擇工具:", tool_config.get("display_name")) + self.current_tool_config = tool_config - # Verify device compatibility - compatible_devices = tool_config.get("compatible_devices", []) - if compatible_devices and dongle not in compatible_devices: - msgBox = QMessageBox(self.main_window) - msgBox.setIcon(QMessageBox.Warning) - msgBox.setWindowTitle("Device Incompatible") - msgBox.setText(f"The selected model does not support {dongle} device.\nSupported devices: {', '.join(compatible_devices)}") - msgBox.setStyleSheet("QLabel { color: white; } QMessageBox { background-color: #2b2b2b; }") - msgBox.exec_() - return False + # 獲取模式和模型名稱 + mode = tool_config.get("mode", "") + model_name = tool_config.get("model_name", "") + + # 載入詳細模型配置 + model_path = os.path.join(UTILS_DIR, mode, model_name) + model_config_path = os.path.join(model_path, "config.json") + + if os.path.exists(model_config_path): + try: + with open(model_config_path, "r", encoding="utf-8") as f: + detailed_config = json.load(f) + tool_config = {**tool_config, **detailed_config} + except Exception as e: + print(f"讀取模型配置時發生錯誤: {e}") + + # 獲取工具輸入類型 + input_info = tool_config.get("input_info", {}) + tool_type = input_info.get("type", "video") + once_mode = True if tool_type == "image" else False + + # 檢查是否從視訊模式切換到圖片模式,或從圖片模式切換到視訊模式 + previous_tool_type = "video" + if hasattr(self, 'previous_tool_config') and self.previous_tool_config: + previous_input_info = self.previous_tool_config.get("input_info", {}) + previous_tool_type = previous_input_info.get("type", "video") + + # 清空推論佇列,確保在模式切換時不會使用舊數據 + self._clear_inference_queue() + + # 儲存當前工具類型以供下次比較 + self.previous_tool_config = tool_config + + # 準備輸入參數 + input_params = tool_config.get("input_parameters", {}).copy() + + # Configure device-related settings + selected_device = self.device_controller.get_selected_device() + if selected_device: + # Get usb_port_id (check if it's a dictionary or object) + if isinstance(selected_device, dict): + input_params["usb_port_id"] = selected_device.get("usb_port_id", 0) + product_id = selected_device.get("product_id", "unknown") + else: + input_params["usb_port_id"] = getattr(selected_device, "usb_port_id", 0) + product_id = getattr(selected_device, "product_id", "unknown") - # Configure firmware paths - scpu_path = os.path.join(FW_DIR, dongle, "fw_scpu.bin") - ncpu_path = os.path.join(FW_DIR, dongle, "fw_ncpu.bin") - input_params["scpu_path"] = scpu_path - input_params["ncpu_path"] = ncpu_path - else: - # Default device handling - devices = self.device_controller.connected_devices - if devices and len(devices) > 0: - input_params["usb_port_id"] = devices[0].get("usb_port_id", 0) - print("Warning: No device specifically selected, using first available device") + # Ensure product_id is in the right format for lookup + # Convert to lowercase hex string if it's a number + if isinstance(product_id, int): + product_id = hex(product_id).lower() + # If it's a string but doesn't start with '0x', add it + elif isinstance(product_id, str) and not product_id.startswith('0x'): + try: + # Try to convert to int first, then to hex format + product_id = hex(int(product_id, 0)).lower() + except ValueError: + # If conversion fails, keep as is + pass + + # Map product_id to dongle type/series + dongle = DongleModelMap.get(product_id, "unknown") + print(f"Selected device: product_id={product_id}, mapped to={dongle}") + + # Verify device compatibility + compatible_devices = tool_config.get("compatible_devices", []) + if compatible_devices and dongle not in compatible_devices: + msgBox = QMessageBox(self.main_window) + msgBox.setIcon(QMessageBox.Warning) + msgBox.setWindowTitle("Device Incompatible") + msgBox.setText(f"The selected model does not support {dongle} device.\nSupported devices: {', '.join(compatible_devices)}") + msgBox.setStyleSheet("QLabel { color: white; } QMessageBox { background-color: #2b2b2b; }") + msgBox.exec_() + return False + + # Configure firmware paths + scpu_path = os.path.join(FW_DIR, dongle, "fw_scpu.bin") + ncpu_path = os.path.join(FW_DIR, dongle, "fw_ncpu.bin") + input_params["scpu_path"] = scpu_path + input_params["ncpu_path"] = ncpu_path else: - input_params["usb_port_id"] = 0 - print("Warning: No connected devices, using default usb_port_id 0") + # Default device handling + devices = self.device_controller.connected_devices + if devices and len(devices) > 0: + input_params["usb_port_id"] = devices[0].get("usb_port_id", 0) + print("Warning: No device specifically selected, using first available device") + else: + input_params["usb_port_id"] = 0 + print("Warning: No connected devices, using default usb_port_id 0") - # Handle file inputs for image/voice modes - if tool_type in ["image", "voice"]: - if hasattr(self.main_window, "destination") and self.main_window.destination: - input_params["file_path"] = self.main_window.destination - if tool_type == "image": - uploaded_img = cv2.imread(self.main_window.destination) - if uploaded_img is not None: - if not self.inference_queue.full(): - self.inference_queue.put(uploaded_img) - print("Uploaded image added to inference queue") + # Handle file inputs for image/voice modes + if tool_type in ["image", "voice"]: + if hasattr(self.main_window, "destination") and self.main_window.destination: + input_params["file_path"] = self.main_window.destination + if tool_type == "image": + uploaded_img = cv2.imread(self.main_window.destination) + if uploaded_img is not None: + if not self.inference_queue.full(): + self.inference_queue.put(uploaded_img) + print("Uploaded image added to inference queue") + else: + print("Warning: inference queue is full") else: - print("Warning: inference queue is full") - else: - print("Warning: Unable to read uploaded image") - else: - input_params["file_path"] = "" - print(f"Warning: {tool_type} mode requires a file input, but no file has been uploaded.") - - # Add model file path - if "model_file" in tool_config: - model_file = tool_config["model_file"] - model_file_path = os.path.join(model_path, model_file) - input_params["model"] = model_file_path - - print("Input parameters:", input_params) - - # Stop existing inference worker if running - if self.inference_worker: - self.inference_worker.stop() - self.inference_worker = None - - # Create new inference worker - self.inference_worker = InferenceWorkerThread( - self.inference_queue, - mode, - model_name, - min_interval=0.5, - mse_threshold=500, - once_mode=once_mode - ) - self.inference_worker.input_params = input_params - self.inference_worker.inference_result_signal.connect(self.main_window.handle_inference_result) - self.inference_worker.start() - print(f"Inference worker started for module: {mode}/{model_name}") - - # Start camera if needed - if tool_type == "video": - self.main_window.media_controller.start_camera() - else: - print("Tool mode is not video, camera not started") + print("Warning: Unable to read uploaded image") + else: + input_params["file_path"] = "" + print(f"Warning: {tool_type} mode requires a file input, but no file has been uploaded.") - return True + # Add model file path + if "model_file" in tool_config: + model_file = tool_config["model_file"] + model_file_path = os.path.join(model_path, model_file) + input_params["model"] = model_file_path + + print("Input parameters:", input_params) + + # Stop existing inference worker if running + if self.inference_worker: + self.inference_worker.stop() + self.inference_worker = None + + # Create new inference worker + self.inference_worker = InferenceWorkerThread( + self.inference_queue, + mode, + model_name, + min_interval=0.5, + mse_threshold=500, + once_mode=once_mode + ) + self.inference_worker.input_params = input_params + self.inference_worker.inference_result_signal.connect(self.main_window.handle_inference_result) + self.inference_worker.start() + print(f"Inference worker started for module: {mode}/{model_name}") + + # Start camera if needed + if tool_type == "video": + # If camera was previously active but disconnected for image processing + if self._camera_was_active and self.main_window.media_controller.video_thread is not None: + # Reconnect the signal + self.main_window.media_controller.video_thread.change_pixmap_signal.connect( + self.main_window.media_controller.update_image + ) + print("Camera reconnected for video processing") + else: + # Start camera normally + self.main_window.media_controller.start_camera() + else: + # For image tools, temporarily pause the camera but don't stop it completely + # This allows switching back to video tools without restarting the camera + if self.main_window.media_controller.video_thread is not None: + # Save current state to indicate camera was running + self._camera_was_active = True + # Disconnect signal to prevent processing frames during image inference + self.main_window.media_controller.video_thread.change_pixmap_signal.disconnect() + print("Camera paused for image processing") + else: + self._camera_was_active = False + + return True + except Exception as e: + print(f"選擇工具時發生錯誤: {e}") + import traceback + print(traceback.format_exc()) + def _clear_inference_queue(self): + """清空推論佇列中的所有數據""" + try: + # 清空現有佇列 + while not self.inference_queue.empty(): + try: + self.inference_queue.get_nowait() + except queue.Empty: + break + print("推論佇列已清空") + except Exception as e: + print(f"清空推論佇列時發生錯誤: {e}") + def add_frame_to_queue(self, frame): - """Add a frame to the inference queue""" - if not self.inference_queue.full(): - self.inference_queue.put(frame) + """將影格添加到推論佇列""" + try: + # 更新原始影格尺寸 + if frame is not None and hasattr(frame, 'shape'): + height, width = frame.shape[:2] + self.original_frame_width = width + self.original_frame_height = height + + # 添加到佇列 + if not self.inference_queue.full(): + self.inference_queue.put(frame) + except Exception as e: + print(f"添加影格到佇列時發生錯誤: {e}") + import traceback + print(traceback.format_exc()) def stop_inference(self): """Stop the inference worker""" if self.inference_worker: self.inference_worker.stop() - self.inference_worker = None \ No newline at end of file + self.inference_worker = None + + def process_uploaded_image(self, file_path): + """處理上傳的圖片並進行推論""" + try: + if not os.path.exists(file_path): + print(f"錯誤: 檔案不存在 {file_path}") + return + + # 清空推論佇列,確保只處理最新的圖片 + self._clear_inference_queue() + + # 讀取圖片 + img = cv2.imread(file_path) + if img is None: + print(f"錯誤: 無法讀取圖片 {file_path}") + return + + # 更新推論工作器參數 + if self.inference_worker: + self.inference_worker.input_params["file_path"] = file_path + + # 將圖片添加到推論佇列 + if not self.inference_queue.full(): + self.inference_queue.put(img) + print(f"已將圖片 {file_path} 添加到推論佇列") + else: + print("警告: 推論佇列已滿") + else: + print("錯誤: 推論工作器未初始化") + except Exception as e: + print(f"處理上傳圖片時發生錯誤: {e}") + import traceback + print(traceback.format_exc()) \ No newline at end of file diff --git a/src/controllers/media_controller.py b/src/controllers/media_controller.py index 956233c..5a1bae2 100644 --- a/src/controllers/media_controller.py +++ b/src/controllers/media_controller.py @@ -1,8 +1,8 @@ import cv2 import os from PyQt5.QtWidgets import QFileDialog -from PyQt5.QtGui import QPixmap -from PyQt5.QtCore import Qt +from PyQt5.QtGui import QPixmap, QPainter, QPen, QFont, QColor +from PyQt5.QtCore import Qt, QRect from src.models.video_thread import VideoThread from src.utils.image_utils import qimage_to_numpy @@ -15,44 +15,159 @@ class MediaController: self.recording = False self.recording_audio = False self.recorded_frames = [] + self._signal_was_connected = False # Track if signal was previously connected + self._inference_paused = False # 追蹤推論是否暫停 def start_camera(self): - """Start the camera for video capture""" - if self.video_thread is None: - self.video_thread = VideoThread() - self.video_thread.change_pixmap_signal.connect(self.update_image) - self.video_thread.start() - print("Camera started") - else: - print("Camera already running") + """啟動相機進行視訊擷取""" + try: + if self.video_thread is None: + print("初始化相機執行緒...") + # 先清除畫布上的任何文字或圖像 + if hasattr(self.main_window, 'canvas_label'): + self.main_window.canvas_label.clear() + + self.video_thread = VideoThread() + if not self._signal_was_connected: + try: + self.video_thread.change_pixmap_signal.connect(self.update_image) + self._signal_was_connected = True + print("相機信號連接成功") + except Exception as e: + print(f"連接相機信號時發生錯誤: {e}") + + # 啟動相機執行緒 + self.video_thread.start() + print("相機執行緒啟動成功") + else: + print("相機已經在運行中") + except Exception as e: + print(f"啟動相機時發生錯誤: {e}") + import traceback + print(traceback.format_exc()) def stop_camera(self): - """Stop the camera""" - if self.video_thread is not None: - self.video_thread.stop() - self.video_thread = None - print("Camera stopped") + """停止相機""" + try: + if self.video_thread is not None: + print("停止相機執行緒") + # 確保先斷開信號連接 + if self._signal_was_connected: + try: + self.video_thread.change_pixmap_signal.disconnect() + self._signal_was_connected = False + print("已斷開相機信號連接") + except Exception as e: + print(f"斷開信號連接時發生錯誤: {e}") + + # 停止執行緒 + self.video_thread.stop() + self.video_thread = None + print("相機已完全停止") + except Exception as e: + print(f"停止相機時發生錯誤: {e}") + import traceback + print(traceback.format_exc()) def update_image(self, qt_image): - """Update the image display and pass to inference queue""" + """更新圖像顯示並處理推論""" try: - # Update canvas display - canvas_size = self.main_window.canvas_label.size() - scaled_image = qt_image.scaled( - canvas_size.width() - 20, - canvas_size.height() - 20, - Qt.KeepAspectRatio, - Qt.SmoothTransformation - ) - self.main_window.canvas_label.setPixmap(QPixmap.fromImage(scaled_image)) - - # Convert QImage to numpy array and add to inference queue - frame_np = qimage_to_numpy(qt_image) - self.inference_controller.add_frame_to_queue(frame_np) - + # 更新畫布上的圖像 + if hasattr(self.main_window, 'canvas_label'): + pixmap = QPixmap.fromImage(qt_image) + + # 如果有邊界框,繪製它 + if hasattr(self.main_window, 'current_bounding_box') and self.main_window.current_bounding_box is not None: + painter = QPainter(pixmap) + pen = QPen(Qt.red) + pen.setWidth(2) + painter.setPen(pen) + + # 獲取邊界框 + bbox_info = self.main_window.current_bounding_box + + # 檢查邊界框格式 + if isinstance(bbox_info, dict) and "bounding box" in bbox_info: + # 從字典中獲取邊界框座標 + bbox = bbox_info["bounding box"] + if len(bbox) >= 4: + # 繪製矩形 + x1, y1, x2, y2 = bbox[0], bbox[1], bbox[2], bbox[3] + painter.drawRect(QRect(x1, y1, x2 - x1, y2 - y1)) + + # 如果有結果標籤,繪製它 + if "result" in bbox_info: + font = QFont() + font.setPointSize(10) + painter.setFont(font) + painter.setPen(QColor(255, 0, 0)) # 紅色 + + # 計算標籤位置(邊界框上方) + label_x = x1 + label_y = y1 - 10 + + # 確保標籤在畫布範圍內 + if label_y < 10: + label_y = y2 + 15 # 如果上方空間不足,放在底部 + + painter.drawText(label_x, label_y, bbox_info["result"]) + elif isinstance(bbox_info, list) and len(bbox_info) >= 4: + # 直接使用列表作為邊界框座標 + x1, y1, x2, y2 = bbox_info[0], bbox_info[1], bbox_info[2], bbox_info[3] + painter.drawRect(QRect(x1, y1, x2 - x1, y2 - y1)) + + # 如果有標籤,繪製它 + if len(bbox_info) > 4 and bbox_info[4]: + font = QFont() + font.setPointSize(10) + painter.setFont(font) + painter.setPen(QColor(255, 0, 0)) # 紅色 + + # 計算標籤位置(邊界框上方) + label_x = x1 + label_y = y1 - 10 + + # 確保標籤在畫布範圍內 + if label_y < 10: + label_y = y2 + 15 # 如果上方空間不足,放在底部 + + painter.drawText(label_x, label_y, bbox_info[4]) + + painter.end() + + # 顯示圖像 + self.main_window.canvas_label.setPixmap(pixmap) + + # 只有在推論未暫停時才將影格添加到推論佇列 + if not self._inference_paused: + frame_np = qimage_to_numpy(qt_image) + self.inference_controller.add_frame_to_queue(frame_np) + except Exception as e: - print(f"Error in update_image: {e}") - + print(f"更新圖像時發生錯誤: {e}") + import traceback + print(traceback.format_exc()) + + def reconnect_camera_signal(self): + """Reconnect the camera signal if it was previously disconnected""" + if self.video_thread is not None and self._signal_was_connected: + try: + # Check if the signal is already connected to avoid duplicate connections + # PyQt doesn't provide a direct way to check if a signal is connected, + # so we use a try-except block + try: + # Attempt to disconnect first to avoid multiple connections + self.video_thread.change_pixmap_signal.disconnect(self.update_image) + except TypeError: + # Signal was not connected, which is fine + pass + + # Reconnect the signal + self.video_thread.change_pixmap_signal.connect(self.update_image) + print("Camera signal reconnected") + except Exception as e: + print(f"Error reconnecting camera signal: {e}") + def record_video(self, button=None): """Start or stop video recording""" if not self.recording: @@ -163,4 +278,26 @@ class MediaController: self.main_window.canvas_label.pixmap().save(filename) print(f"Screenshot saved to {filename}") except Exception as e: - print(f"Error taking screenshot: {e}") \ No newline at end of file + print(f"Error taking screenshot: {e}") + + def toggle_inference_pause(self): + """切換推論暫停狀態""" + try: + self._inference_paused = not self._inference_paused + if self._inference_paused: + # 暫停時清除邊界框 + self.main_window.current_bounding_box = None + else: + # 恢復推論時,確保相機仍在運行 + if self.video_thread is None or not self.video_thread.isRunning(): + self.start_camera() + return self._inference_paused + except Exception as e: + print(f"切換推論暫停狀態時發生錯誤: {e}") + import traceback + print(traceback.format_exc()) + return self._inference_paused + + def is_inference_paused(self): + """檢查推論是否暫停""" + return self._inference_paused \ No newline at end of file diff --git a/src/models/inference_worker.py b/src/models/inference_worker.py index c28cd65..51364a7 100644 --- a/src/models/inference_worker.py +++ b/src/models/inference_worker.py @@ -43,13 +43,27 @@ class InferenceWorkerThread(QThread): continue if self.last_frame is not None: - mse = np.mean((frame.astype(np.float32) - self.last_frame.astype(np.float32)) ** 2) - if mse < self.mse_threshold and self.cached_result is not None: - self.inference_result_signal.emit(self.cached_result) - if self.once_mode: - self._running = False - break - continue + # 檢查當前幀與上一幀的尺寸是否相同 + if frame.shape != self.last_frame.shape: + print(f"幀尺寸變更: 從 {self.last_frame.shape} 變更為 {frame.shape}") + # 尺寸不同時,重置上一幀和緩存結果 + self.last_frame = None + self.cached_result = None + else: + # 只有在尺寸相同時才進行 MSE 計算 + try: + mse = np.mean((frame.astype(np.float32) - self.last_frame.astype(np.float32)) ** 2) + if mse < self.mse_threshold and self.cached_result is not None: + self.inference_result_signal.emit(self.cached_result) + if self.once_mode: + self._running = False + break + continue + except Exception as e: + print(f"計算 MSE 時發生錯誤: {e}") + # 發生錯誤時重置上一幀和緩存結果 + self.last_frame = None + self.cached_result = None try: result = self.inference_module.inference(frame, params=self.input_params) diff --git a/src/models/video_thread.py b/src/models/video_thread.py index 250952b..3b8ec2f 100644 --- a/src/models/video_thread.py +++ b/src/models/video_thread.py @@ -8,23 +8,78 @@ class VideoThread(QThread): def __init__(self): super().__init__() self._run_flag = True + self._camera_open_attempts = 0 + self._max_attempts = 3 def run(self): - cap = cv2.VideoCapture(0) - if not cap.isOpened(): - print("Cannot open camera") - self._run_flag = False - while self._run_flag: - ret, frame = cap.read() - if ret: - # Convert to RGB format - frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - height, width, channel = frame.shape - bytes_per_line = channel * width - qt_image = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888) - self.change_pixmap_signal.emit(qt_image) - cap.release() + # 嘗試多次開啟相機 + 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(): + print("無法使用DirectShow開啟相機,嘗試預設後端") + cap = cv2.VideoCapture(0) + if not cap.isOpened(): + print(f"無法使用任何後端開啟相機,等待1秒後重試...") + import time + time.sleep(1) + continue + + # 設置相機屬性以獲得更好的性能 + # 降低解析度以提高啟動速度和幀率 + cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) + cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) + cap.set(cv2.CAP_PROP_FPS, 30) + + # 設置緩衝區大小為1,減少延遲 + cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) + + # 預熱相機,丟棄前幾幀以加快穩定速度 + for _ in range(5): + cap.read() + + # 相機開啟成功,重置嘗試計數 + self._camera_open_attempts = 0 + + # 主循環 + while self._run_flag: + ret, frame = cap.read() + if ret: + # 轉換為RGB格式 + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + height, width, channel = frame.shape + bytes_per_line = channel * width + qt_image = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888) + self.change_pixmap_signal.emit(qt_image) + else: + print("無法讀取相機幀,相機可能已斷開連接") + break + + # 釋放相機資源 + cap.release() + + # 如果是因為停止信號而退出循環,則不再重試 + if not self._run_flag: + break + + print("相機連接中斷,嘗試重新連接...") + + if self._camera_open_attempts >= self._max_attempts: + print("達到最大嘗試次數,無法開啟相機") def stop(self): - self._run_flag = False - self.wait() \ No newline at end of file + """停止執行緒""" + try: + print("正在停止相機執行緒...") + self._run_flag = False + # 等待執行緒完成 + if self.isRunning(): + self.wait() + print("相機執行緒已停止") + except Exception as e: + print(f"停止相機執行緒時發生錯誤: {e}") + import traceback + print(traceback.format_exc()) \ No newline at end of file diff --git a/src/services/device_service.py b/src/services/device_service.py index def1533..ed0cedc 100644 --- a/src/services/device_service.py +++ b/src/services/device_service.py @@ -1,46 +1,47 @@ -# import kp - -# def check_available_device(): -# try: -# print("checking available devices") -# device_descriptors = kp.core.scan_devices() -# return device_descriptors -# except Exception as e: -# print(f"Error scanning devices: {e}") -# # 返回一個空的設備描述符或模擬數據 -# class EmptyDescriptor: -# def __init__(self): -# self.device_descriptor_number = 0 -# self.device_descriptor_list = [] -# return EmptyDescriptor() +import kp def check_available_device(): - # 模擬設備描述符 - print("checking available devices") - class EmptyDescriptor: - def __init__(self): - self.device_descriptor_number = 0 - self.device_descriptor_list = [{ - "usb_port_id": 4, - "vendor_id": "0x3231", - "product_id": "0x720", - "link_speed": "UsbSpeed.KP_USB_SPEED_SUPER", - "kn_number": "0xB306224C", - "is_connectable": True, - "usb_port_path": "4-1", - "firmware": "KDP2 Comp/F" - }, - { - "usb_port_id": 5, - "vendor_id": "0x3231", - "product_id": "0x520", - "link_speed": "UsbSpeed.KP_USB_SPEED_SUPER", - "kn_number": "0xB306224C", - "is_connectable": True, - "usb_port_path": "4-1", - "firmware": "KDP2 Comp/F" - }] - return EmptyDescriptor() + 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: + def __init__(self): + self.device_descriptor_number = 0 + self.device_descriptor_list = [] + return EmptyDescriptor() + +# def check_available_device(): + # # 模擬設備描述符 + # print("checking available devices") + # class EmptyDescriptor: + # def __init__(self): + # self.device_descriptor_number = 0 + # self.device_descriptor_list = [{ + # "usb_port_id": 4, + # "vendor_id": "0x3231", + # "product_id": "0x720", + # "link_speed": "UsbSpeed.KP_USB_SPEED_SUPER", + # "kn_number": "0xB306224C", + # "is_connectable": True, + # "usb_port_path": "4-1", + # "firmware": "KDP2 Comp/F" + # }, + # { + # "usb_port_id": 5, + # "vendor_id": "0x3231", + # "product_id": "0x520", + # "link_speed": "UsbSpeed.KP_USB_SPEED_SUPER", + # "kn_number": "0xB306224C", + # "is_connectable": True, + # "usb_port_path": "4-1", + # "firmware": "KDP2 Comp/F" + # }] + # return EmptyDescriptor() # device_descriptors = [ # { # "usb_port_id": 4, diff --git a/src/services/file_service.py b/src/services/file_service.py index 9616347..77d338c 100644 --- a/src/services/file_service.py +++ b/src/services/file_service.py @@ -1,70 +1,177 @@ import os import shutil -from PyQt5.QtWidgets import QFileDialog, QMessageBox +from PyQt5.QtWidgets import QFileDialog, QMessageBox, QApplication +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QImage, QPixmap +import cv2 class FileService: def __init__(self, main_window, upload_dir): self.main_window = main_window self.upload_dir = upload_dir self.destination = None + self._camera_was_active = False # Track if camera was active before upload def upload_file(self): - """Handle file upload process""" + """處理檔案上傳流程""" try: - print("Calling QFileDialog.getOpenFileName") + # 1. 先完全停止相機(如果正在運行) + if hasattr(self.main_window, 'media_controller') and self.main_window.media_controller.video_thread is not None: + print("上傳前停止相機") + try: + # 儲存狀態以指示相機正在運行 + self._camera_was_active = True + # 顯示上傳中提示 + if hasattr(self.main_window, 'canvas_label'): + self.main_window.canvas_label.setText("準備上傳檔案...") + self.main_window.canvas_label.setAlignment(Qt.AlignCenter) + self.main_window.canvas_label.setStyleSheet("color: white; font-size: 24px;") + # 確保 UI 更新 + QApplication.processEvents() + + # 只暫停推論,不完全停止相機 + if not self.main_window.media_controller._inference_paused: + self.main_window.media_controller.toggle_inference_pause() + + # 斷開信號連接但不停止相機執行緒 + if hasattr(self.main_window.media_controller, '_signal_was_connected') and self.main_window.media_controller._signal_was_connected: + try: + self.main_window.media_controller.video_thread.change_pixmap_signal.disconnect() + self.main_window.media_controller._signal_was_connected = False + print("已暫時斷開相機信號連接") + except Exception as e: + print(f"斷開信號連接時發生錯誤: {e}") + except Exception as e: + print(f"準備上傳時發生錯誤: {e}") + import traceback + print(traceback.format_exc()) + # 即使發生錯誤,也繼續嘗試上傳 + else: + self._camera_was_active = False + + print("呼叫 QFileDialog.getOpenFileName") options = QFileDialog.Options() file_path, _ = QFileDialog.getOpenFileName( self.main_window, - "Upload File", + "上傳檔案", "", - "All Files (*)", + "所有檔案 (*)", options=options ) - print("File path obtained:", file_path) + print("檔案路徑取得:", file_path) if file_path: - print("Checking if upload directory exists") + print("檢查上傳目錄是否存在") if not os.path.exists(self.upload_dir): os.makedirs(self.upload_dir) - print(f"Created UPLOAD_DIR: {self.upload_dir}") + print(f"建立上傳目錄: {self.upload_dir}") - print("Checking if original file exists:", file_path) + print("檢查原始檔案是否存在:", file_path) if not os.path.exists(file_path): - self.show_message(QMessageBox.Critical, "Error", "Selected file not found") + self.show_message(QMessageBox.Critical, "錯誤", "選擇的檔案不存在") return None file_name = os.path.basename(file_path) self.destination = os.path.join(self.upload_dir, file_name) - print("Target path:", self.destination) + print("目標路徑:", self.destination) - # Check if target path is writable + # 檢查目標路徑是否可寫入 try: - print("Testing file write permission") + print("測試檔案寫入權限") with open(self.destination, 'wb') as test_file: pass os.remove(self.destination) - print("Test file creation and deletion successful") + print("測試檔案建立和刪除成功") except PermissionError: - self.show_message(QMessageBox.Critical, "Error", "Cannot write to target directory") + self.show_message(QMessageBox.Critical, "錯誤", "無法寫入目標目錄") return None - print("Starting file copy") - shutil.copy2(file_path, self.destination) - print("File copy complete") - self.show_message(QMessageBox.Information, "Success", f"File uploaded to: {self.destination}") + print("開始檔案複製") + try: + shutil.copy2(file_path, self.destination) + print("檔案複製成功") + + # 更新主視窗目的地 + self.main_window.destination = self.destination + print(f"更新主視窗目的地: {self.main_window.destination}") + + # 處理上傳的影像 + if self.main_window.inference_controller.current_tool_config: + print("使用推論控制器處理上傳的影像") + # 先在畫布上顯示影像 + try: + # 載入和顯示影像 + image = cv2.imread(self.destination) + if image is not None: + # 轉換為 RGB 顯示 + image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + height, width, channel = image_rgb.shape + bytes_per_line = channel * width + qt_image = QImage(image_rgb.data, width, height, bytes_per_line, QImage.Format_RGB888) + + # 將影像縮放以適應畫布 + canvas_size = self.main_window.canvas_label.size() + scaled_image = qt_image.scaled( + int(canvas_size.width() * 0.95), + int(canvas_size.height() * 0.95), + Qt.KeepAspectRatio, + Qt.SmoothTransformation + ) + self.main_window.canvas_label.setPixmap(QPixmap.fromImage(scaled_image)) + print("影像顯示在畫布上") + except Exception as e: + print(f"顯示影像時發生錯誤: {e}") + import traceback + print(traceback.format_exc()) + + # 然後使用推論處理它 + self.main_window.inference_controller.process_uploaded_image(self.destination) + + return self.destination + except Exception as e: + import traceback + print("檔案複製過程中發生錯誤:\n", traceback.format_exc()) + self.show_message(QMessageBox.Critical, "錯誤", f"上傳錯誤: {str(e)}") + return None - return self.destination - return None except Exception as e: import traceback - print("Exception during upload process:\n", traceback.format_exc()) - self.show_message(QMessageBox.Critical, "Error", f"Upload error: {str(e)}") + print("上傳過程中發生錯誤:\n", traceback.format_exc()) + self.show_message(QMessageBox.Critical, "錯誤", f"上傳錯誤: {str(e)}") return None - + finally: + # 如果相機之前是活動的,嘗試恢復相機連接 + if self._camera_was_active and hasattr(self.main_window, 'media_controller'): + try: + # 延遲一點時間以確保處理完成 + QApplication.processEvents() + + # 如果相機執行緒仍然存在但信號已斷開,重新連接信號 + if (self.main_window.media_controller.video_thread is not None and + not self.main_window.media_controller._signal_was_connected): + try: + self.main_window.media_controller.video_thread.change_pixmap_signal.connect( + self.main_window.media_controller.update_image + ) + self.main_window.media_controller._signal_was_connected = True + print("已重新連接相機信號") + except Exception as e: + print(f"重新連接相機信號時發生錯誤: {e}") + + # 如果推論被暫停,恢復推論 + if self.main_window.media_controller._inference_paused: + self.main_window.media_controller.toggle_inference_pause() + print("已恢復推論") + + except Exception as e: + print(f"恢復相機時發生錯誤: {e}") + import traceback + print(traceback.format_exc()) + def show_message(self, icon, title, message): - """Display a message box with custom styling""" + """顯示自訂樣式的訊息盒""" msgBox = QMessageBox(self.main_window) msgBox.setIcon(icon) msgBox.setWindowTitle(title) diff --git a/src/services/model_service.py b/src/services/model_service.py index e4280ca..225d04d 100644 --- a/src/services/model_service.py +++ b/src/services/model_service.py @@ -1,4 +1,4 @@ -# import kp +import kp import cv2, os, shutil, sys from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QLabel, QPushButton, QComboBox, QFileDialog, QMessageBox, QHBoxLayout, QDialog, QListWidget, diff --git a/src/views/components/canvas_area.py b/src/views/components/canvas_area.py index fbdbec9..deed871 100644 --- a/src/views/components/canvas_area.py +++ b/src/views/components/canvas_area.py @@ -15,6 +15,7 @@ def create_canvas_area(parent): canvas_label = QLabel() canvas_label.setAlignment(Qt.AlignCenter) canvas_label.setStyleSheet("border: none; background: transparent;") + canvas_label.setMinimumSize(880, 730) # Set minimum size to ensure proper display canvas_layout.addWidget(canvas_label) return canvas_frame, canvas_label diff --git a/src/views/components/device_list.py b/src/views/components/device_list.py index 27bd654..1fb08fd 100644 --- a/src/views/components/device_list.py +++ b/src/views/components/device_list.py @@ -1,10 +1,10 @@ -from PyQt5.QtWidgets import QFrame, QVBoxLayout, QHBoxLayout, QLabel, QListWidget, QWidget, QPushButton +from PyQt5.QtWidgets import QFrame, QVBoxLayout, QHBoxLayout, QLabel, QListWidget, QWidget, QPushButton, QListWidgetItem from PyQt5.QtSvg import QSvgWidget from PyQt5.QtCore import Qt -from PyQt5.QtGui import QPixmap +from PyQt5.QtGui import QPixmap, QIcon import os -from src.config import SECONDARY_COLOR, UXUI_ASSETS, BUTTON_STYLE, DongleIconMap +from src.config import SECONDARY_COLOR, UXUI_ASSETS, BUTTON_STYLE, DongleIconMap, DongleModelMap def create_device_layout(parent, device_controller): """Create the device list layout""" @@ -40,6 +40,26 @@ def create_device_layout(parent, device_controller): # Device list device_list_widget = QListWidget(parent) + device_list_widget.setStyleSheet(""" + QListWidget { + background-color: transparent; + border: none; + color: white; + } + QListWidget::item { + padding: 5px; + border-radius: 5px; + } + QListWidget::item:selected { + background-color: rgba(255, 255, 255, 0.2); + } + """) + + # Connect item selection signal + device_list_widget.itemClicked.connect(lambda item: device_controller.select_device( + item.data(Qt.UserRole), item, device_list_widget + )) + devices_layout.addWidget(device_list_widget) # Detail button diff --git a/src/views/components/device_popup.py b/src/views/components/device_popup.py index c2da98f..b9ba9bb 100644 --- a/src/views/components/device_popup.py +++ b/src/views/components/device_popup.py @@ -1,9 +1,10 @@ -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QListWidget, QListWidgetItem +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QListWidget, QListWidgetItem, QFrame, QGridLayout, QSizePolicy from PyQt5.QtSvg import QSvgWidget -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, QSize, QMargins +from PyQt5.QtGui import QPixmap, QIcon, QColor import os -from src.config import SECONDARY_COLOR, BUTTON_STYLE, UXUI_ASSETS +from src.config import SECONDARY_COLOR, BUTTON_STYLE, UXUI_ASSETS, DongleIconMap def create_device_popup(parent, device_controller): """Create a popup window for device connection management""" @@ -19,10 +20,34 @@ def create_device_popup(parent, device_controller): border-radius: 20px; padding: 20px; }} + QLabel {{ + color: white; + }} + QListWidget {{ + background-color: rgba(255, 255, 255, 0.1); + border-radius: 10px; + padding: 5px; + color: white; + }} + QListWidget::item {{ + border-radius: 5px; + background-color: rgba(255, 255, 255, 0.1); + margin-bottom: 5px; + }} + QListWidget::item:selected {{ + background-color: rgba(52, 152, 219, 0.5); + }} + QFrame.device-info {{ + background-color: rgba(255, 255, 255, 0.1); + border-radius: 10px; + padding: 10px; + margin-top: 15px; + }} """) popup_layout = QVBoxLayout(popup) - popup_layout.setContentsMargins(0, 0, 0, 0) + popup_layout.setContentsMargins(20, 20, 20, 20) + popup_layout.setSpacing(15) # Title row title_layout = QHBoxLayout() @@ -37,40 +62,253 @@ def create_device_popup(parent, device_controller): container_layout.addWidget(device_icon) popup_label = QLabel("Device Connection") - popup_label.setStyleSheet("color: white; font-size: 32px;") + popup_label.setStyleSheet("color: white; font-size: 32px; font-weight: bold;") container_layout.addWidget(popup_label) container_layout.setAlignment(Qt.AlignCenter) title_layout.addWidget(title_container) popup_layout.addLayout(title_layout) - - # Device list + + # Device list section - 設置為可捲動 + list_section = QFrame(popup) + list_section.setStyleSheet("border: none; background: transparent;") + list_layout = QVBoxLayout(list_section) + list_layout.setContentsMargins(0, 0, 0, 0) + + # Device list - 設置為可捲動的列表 device_list_widget_popup = QListWidget(popup) - popup_layout.addWidget(device_list_widget_popup) + device_list_widget_popup.setMinimumHeight(250) # 增加整個列表的高度 + # 啟用垂直捲動條 + device_list_widget_popup.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + # 設置列表項高度和圖示大小 + device_list_widget_popup.setIconSize(QSize(15, 15)) # 增大圖示 + list_layout.addWidget(device_list_widget_popup) # Store reference to this list widget for later use parent.device_list_widget_popup = device_list_widget_popup + + # Comment out the device details section + """ + # Device details section (initially hidden) + device_details = QFrame(popup) + device_details.setObjectName("device-details") + device_details.setProperty("class", "device-info") + device_details.setVisible(False) # Initially hidden + + details_layout = QGridLayout(device_details) + details_layout.setColumnStretch(1, 1) + # 增加行間距 + details_layout.setVerticalSpacing(2) + + # Device Type + device_type_label_title = QLabel("Device Type:") + device_type_label_title.setStyleSheet("font-size: 14px;") + details_layout.addWidget(device_type_label_title, 0, 0) + device_type_label = QLabel("-") + device_type_label.setObjectName("device-type") + device_type_label.setStyleSheet("font-size: 14px;") + details_layout.addWidget(device_type_label, 0, 1) + + # Port ID + port_id_label_title = QLabel("Port ID:") + port_id_label_title.setStyleSheet("font-size: 14px;") + details_layout.addWidget(port_id_label_title, 1, 0) + port_id_label = QLabel("-") + port_id_label.setObjectName("port-id") + port_id_label.setStyleSheet("font-size: 14px;") + details_layout.addWidget(port_id_label, 1, 1) + + # KN Number + kn_number_label_title = QLabel("KN Number:") + kn_number_label_title.setStyleSheet("font-size: 14px;") + details_layout.addWidget(kn_number_label_title, 2, 0) + kn_number_label = QLabel("-") + kn_number_label.setObjectName("kn-number") + kn_number_label.setStyleSheet("font-size: 14px;") + details_layout.addWidget(kn_number_label, 2, 1) + + # Status + status_label_title = QLabel("Status:") + status_label_title.setStyleSheet("font-size: 14px;") + details_layout.addWidget(status_label_title, 3, 0) + status_label = QLabel("-") + status_label.setObjectName("status") + status_label.setStyleSheet("font-size: 14px;") + details_layout.addWidget(status_label, 3, 1) + + # Store references to labels + parent.device_detail_labels = { + "device-type": device_type_label, + "port-id": port_id_label, + "kn-number": kn_number_label, + "status": status_label, + "frame": device_details + } + + # Connect item selection to show details + device_list_widget_popup.itemClicked.connect(lambda item: show_device_details(parent, item)) + + list_layout.addWidget(device_details) + """ + popup_layout.addWidget(list_section) # Button area button_layout = QHBoxLayout() + button_layout.setAlignment(Qt.AlignCenter) refresh_button = QPushButton("Refresh") - refresh_button.clicked.connect(device_controller.refresh_devices) - refresh_button.setFixedSize(110, 45) + refresh_button.clicked.connect(lambda: refresh_devices(parent, device_controller)) + refresh_button.setFixedSize(150, 45) refresh_button.setStyleSheet(BUTTON_STYLE) button_layout.addWidget(refresh_button) done_button = QPushButton("Done") done_button.setStyleSheet(BUTTON_STYLE) - done_button.setFixedSize(110, 45) + done_button.setFixedSize(150, 45) done_button.clicked.connect(parent.hide_device_popup) button_layout.addWidget(done_button) - button_layout.setSpacing(10) - popup_layout.addSpacing(20) + button_layout.setSpacing(20) + # 減少底部間距,讓按鈕更靠近底部 + popup_layout.addSpacing(5) popup_layout.addLayout(button_layout) return popup except Exception as e: print(f"Error in create_device_popup: {e}") - return QWidget(parent) \ No newline at end of file + return QWidget(parent) + +# Also need to comment out the show_device_details function since it's no longer used + +# def show_device_details(parent, item): +# """Show details for the selected device""" +# try: +# # Get the device data from the item +# device_data = item.data(Qt.UserRole) +# print("device_data", device_data) +# if not device_data: +# return + +# # Update the detail labels +# labels = parent.device_detail_labels + +# # Set values +# labels["device-type"].setText(device_data.get("dongle", "-")) +# labels["port-id"].setText(str(device_data.get("usb_port_id", "-"))) +# labels["kn-number"].setText(str(device_data.get("kn_number", "-"))) +# labels["status"].setText("Connected" if device_data.get("is_connectable", True) else "Not Available") + +# # Show the details frame +# labels["frame"].setVisible(True) +# except Exception as e: +# print(f"Error showing device details: {e}") + + +def refresh_devices(parent, device_controller): + """Refresh the device list and update the UI""" + try: + # Call the refresh method from device controller + device_controller.refresh_devices() + + # Clear the list + parent.device_list_widget_popup.clear() + + # Comment out the details frame visibility setting + """ + # Hide details frame + if hasattr(parent, "device_detail_labels") and "frame" in parent.device_detail_labels: + parent.device_detail_labels["frame"].setVisible(False) + """ + + # Track unique device models to avoid duplicates + seen_models = set() + + # Add devices to the list + for device in device_controller.connected_devices: + try: + item = QListWidgetItem() + + # Store the device data + item.setData(Qt.UserRole, device) + + # 使用自定義 widget 來顯示設備信息 + widget = QWidget() + widget.setFixedHeight(60) # 固定高度 + + # 使用水平佈局 + main_layout = QHBoxLayout(widget) + # 減少內邊距保持足夠空間 + main_layout.setContentsMargins(8, 4, 8, 4) + main_layout.setSpacing(5) + + # 左側圖示容器(帶有背景色) + icon_container = QFrame() + icon_container.setFixedSize(30, 30) + icon_container.setStyleSheet("background-color: #182D4B; border-radius: 5px;") + + icon_layout = QVBoxLayout(icon_container) + icon_layout.setContentsMargins(0, 0, 0, 0) + icon_layout.setAlignment(Qt.AlignCenter) + + # 獲取設備圖示 + product_id = hex(device.get("product_id", 0)).strip().lower() + icon_path = os.path.join(UXUI_ASSETS, "Assets_png", DongleIconMap.get(product_id, "unknown_dongle.png")) + + if os.path.exists(icon_path): + icon_label = QLabel() + pixmap = QPixmap(icon_path) + # 確保圖標大小適中 + scaled_pixmap = pixmap.scaled(30, 30, Qt.KeepAspectRatio, Qt.SmoothTransformation) + icon_label.setPixmap(scaled_pixmap) + icon_layout.addWidget(icon_label) + + main_layout.addWidget(icon_container) + + # 右側信息區域 + info_container = QWidget() + info_layout = QVBoxLayout(info_container) + info_layout.setContentsMargins(0, 0, 0, 0) + # info_layout.setSpacing(2) + + # 設備名稱和型號 + dongle_name = device.get("dongle", "Unknown Device") + kn_number = device.get("kn_number", "Unknown") + status = "Connected" if device.get("is_connectable", True) else "Not Available" + + # 只顯示型號名稱,避免重複 + device_model_key = f"{dongle_name}_{product_id}" + if device_model_key in seen_models: + # 只顯示狀態 + device_label_text = "" + else: + seen_models.add(device_model_key) + device_label_text = f"{dongle_name}" + + device_label = QLabel(device_label_text) + device_label.setStyleSheet("color: white; font-weight: bold; font-size: 16px;") + info_layout.addWidget(device_label) + + # KN 號碼 + kn_label = QLabel(f"KN: {kn_number}") + kn_label.setStyleSheet("color: white; font-size: 14px;") + info_layout.addWidget(kn_label) + + # 將信息容器添加到主佈局 + main_layout.addWidget(info_container, 1) # 設置伸展因子為1 + + # 狀態標籤(右側) + status_label = QLabel(status) + status_label.setStyleSheet("color: white; font-size: 12px;") + main_layout.addWidget(status_label, 0, Qt.AlignRight | Qt.AlignVCenter) # 右對齊 + + # 設置自定義 widget 為列表項的 widget + parent.device_list_widget_popup.addItem(item) + parent.device_list_widget_popup.setItemWidget(item, widget) + + # 設置項目大小 + item.setSizeHint(widget.sizeHint()) + + except Exception as e: + print(f"Error adding device to list: {e}") + except Exception as e: + print(f"Error refreshing devices: {e}") \ No newline at end of file diff --git a/src/views/components/media_panel.py b/src/views/components/media_panel.py index bd0b90c..0879d2a 100644 --- a/src/views/components/media_panel.py +++ b/src/views/components/media_panel.py @@ -14,15 +14,22 @@ def create_media_panel(parent, media_controller, file_service): media_layout = QVBoxLayout(media_panel) media_layout.setAlignment(Qt.AlignCenter) + # 確保使用正確的路徑分隔符 + assets_path = UXUI_ASSETS.replace('\\', '/') + if not assets_path.endswith('/'): + assets_path += '/' + # Media button information media_buttons_info = [ - ('screenshot', os.path.join(UXUI_ASSETS, "Assets_svg/bt_function_screencapture_normal.svg"), + ('screenshot', os.path.join(assets_path, "Assets_svg/bt_function_screencapture_normal.svg").replace('\\', '/'), media_controller.take_screenshot), - ('upload file', os.path.join(UXUI_ASSETS, "Assets_svg/bt_function_upload_normal.svg"), + ('upload file', os.path.join(assets_path, "Assets_svg/bt_function_upload_normal.svg").replace('\\', '/'), file_service.upload_file), - ('voice', os.path.join(UXUI_ASSETS, "Assets_svg/ic_recording_voice.svg"), + ('pause/resume', os.path.join(assets_path, "Assets_svg/btn_result_image_delete_hover.svg").replace('\\', '/'), + lambda: toggle_pause_button(parent, media_controller)), + ('voice', os.path.join(assets_path, "Assets_svg/ic_recording_voice.svg").replace('\\', '/'), lambda: media_controller.record_audio(None)), - ('video', os.path.join(UXUI_ASSETS, "Assets_svg/ic_recording_camera.svg"), + ('video', os.path.join(assets_path, "Assets_svg/ic_recording_camera.svg").replace('\\', '/'), lambda: media_controller.record_video(None)), ] @@ -52,13 +59,43 @@ def create_media_panel(parent, media_controller, file_service): icon.setFixedSize(40, 40) button_layout.addWidget(icon) + # 為暫停按鈕儲存參考 + if button_name == 'pause/resume': + parent.pause_button = button + parent.pause_icon = icon + parent.pause_icon_path = icon_path + parent.play_icon_path = os.path.join(assets_path, "Assets_svg/bt_function_video_hover.svg").replace('\\', '/') + button.clicked.connect(callback) media_layout.addWidget(button, alignment=Qt.AlignCenter) media_panel.setLayout(media_layout) - media_panel.setFixedSize(90, 240) + media_panel.setFixedSize(90, 290) # 增加高度以容納新按鈕 return media_panel except Exception as e: print(f"Error in create_media_panel: {e}") - return QFrame(parent) \ No newline at end of file + return QFrame(parent) + +def toggle_pause_button(parent, media_controller): + """切換暫停/恢復按鈕圖示並觸發相應功能""" + try: + is_paused = media_controller.toggle_inference_pause() + + # 切換圖示 + if is_paused: + # 檢查文件是否存在 + if os.path.exists(parent.play_icon_path): + parent.pause_icon.load(parent.play_icon_path) + else: + print(f"警告: 播放圖標文件不存在: {parent.play_icon_path}") + else: + # 檢查文件是否存在 + if os.path.exists(parent.pause_icon_path): + parent.pause_icon.load(parent.pause_icon_path) + else: + print(f"警告: 暫停圖標文件不存在: {parent.pause_icon_path}") + except Exception as e: + print(f"切換暫停按鈕時發生錯誤: {e}") + import traceback + print(traceback.format_exc()) \ No newline at end of file diff --git a/src/views/login_screen.py b/src/views/login_screen.py index 0a7383d..a1fc8e6 100644 --- a/src/views/login_screen.py +++ b/src/views/login_screen.py @@ -17,116 +17,181 @@ class LoginScreen(QWidget): def init_ui(self): # Basic window setup self.setGeometry(100, 100, *WINDOW_SIZE) - self.setStyleSheet(f"background-color: {BACKGROUND_COLOR};") + self.setStyleSheet(f"background-color: #F5F7FA;") # Light gray background # Main layout layout = QVBoxLayout(self) + layout.setContentsMargins(40, 40, 40, 40) + layout.setSpacing(20) + + # Header with logo + header_frame = QFrame(self) + header_frame.setStyleSheet("background-color: #2C3E50; border-radius: 10px;") + header_frame.setFixedHeight(100) + + header_layout = QHBoxLayout(header_frame) + header_layout.setContentsMargins(20, 0, 20, 0) # Logo logo_label = QLabel(self) logo_path = os.path.join(UXUI_ASSETS, "Assets_png/kneron_logo.png") if os.path.exists(logo_path): logo_pixmap = QPixmap(logo_path) - logo_label.setPixmap(logo_pixmap) + scaled_logo = logo_pixmap.scaled(150, 60, Qt.KeepAspectRatio, Qt.SmoothTransformation) + logo_label.setPixmap(scaled_logo) logo_label.setAlignment(Qt.AlignCenter) - layout.addWidget(logo_label) - # Title - title_label = QLabel("Login", self) - title_label.setAlignment(Qt.AlignCenter) - title_label.setFont(QFont("Arial", 24, QFont.Bold)) - layout.addWidget(title_label) + header_layout.addWidget(logo_label, alignment=Qt.AlignCenter) + layout.addWidget(header_frame) # Login form container form_container = QFrame(self) - form_container.setFrameShape(QFrame.StyledPanel) form_container.setStyleSheet(""" QFrame { background-color: white; border-radius: 10px; - padding: 20px; + border: 1px solid #E0E0E0; } """) form_layout = QVBoxLayout(form_container) + form_layout.setContentsMargins(30, 30, 30, 30) + form_layout.setSpacing(20) + + # Title + title_label = QLabel("Login", self) + title_label.setAlignment(Qt.AlignCenter) + title_label.setStyleSheet("font-size: 28px; font-weight: bold; color: #2C3E50; margin-bottom: 10px;") + form_layout.addWidget(title_label) # Server type server_label = QLabel("Server Authentication Type", self) - server_label.setFont(QFont("Arial", 12)) + server_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #2C3E50;") form_layout.addWidget(server_label) self.server_combo = QComboBox(self) self.server_combo.addItems(["Standard Password Authentication", "Other Authentication Method"]) - self.server_combo.setFont(QFont("Arial", 10)) self.server_combo.setMinimumHeight(40) + self.server_combo.setStyleSheet(""" + QComboBox { + border: 1px solid #E0E0E0; + border-radius: 5px; + padding: 5px 10px; + background-color: white; + font-size: 14px; + color: #2C3E50; + } + QComboBox::drop-down { + border: none; + width: 30px; + } + QComboBox::down-arrow { + image: url(""" + os.path.join(UXUI_ASSETS, "Assets_svg/ic_result_folder_normal.svg").replace("\\", "/") + """); + width: 12px; + height: 12px; + } + QComboBox QAbstractItemView { + border: 1px solid #E0E0E0; + selection-background-color: #ECF0F1; + selection-color: #2C3E50; + background-color: white; + outline: none; + } + """) form_layout.addWidget(self.server_combo) - form_layout.addSpacing(10) - # Username username_label = QLabel("Username", self) - username_label.setFont(QFont("Arial", 12)) + username_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #2C3E50;") form_layout.addWidget(username_label) self.username_input = QLineEdit(self) self.username_input.setPlaceholderText("Enter your username") self.username_input.setMinimumHeight(40) - self.username_input.setFont(QFont("Arial", 10)) + self.username_input.setStyleSheet(""" + QLineEdit { + border: 1px solid #E0E0E0; + border-radius: 5px; + padding: 5px 10px; + background-color: white; + font-size: 14px; + color: #2C3E50; + } + QLineEdit:focus { + border: 1px solid #3498DB; + } + """) form_layout.addWidget(self.username_input) - form_layout.addSpacing(10) - # Password password_label = QLabel("Password", self) - password_label.setFont(QFont("Arial", 12)) + password_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #2C3E50;") form_layout.addWidget(password_label) self.password_input = QLineEdit(self) self.password_input.setPlaceholderText("Enter your password") self.password_input.setEchoMode(QLineEdit.Password) self.password_input.setMinimumHeight(40) - self.password_input.setFont(QFont("Arial", 10)) + self.password_input.setStyleSheet(""" + QLineEdit { + border: 1px solid #E0E0E0; + border-radius: 5px; + padding: 5px 10px; + background-color: white; + font-size: 14px; + color: #2C3E50; + } + QLineEdit:focus { + border: 1px solid #3498DB; + } + """) form_layout.addWidget(self.password_input) - form_layout.addSpacing(20) - # Error message (hidden by default) self.error_label = QLabel("", self) - self.error_label.setStyleSheet("color: red;") - self.error_label.setFont(QFont("Arial", 10)) + self.error_label.setStyleSheet("color: #E74C3C; font-size: 14px;") self.error_label.hide() form_layout.addWidget(self.error_label) # Buttons button_layout = QHBoxLayout() + button_layout.setSpacing(15) back_button = QPushButton("Back", self) - back_button.setMinimumHeight(40) - back_button.setFont(QFont("Arial", 12)) + back_button.setMinimumHeight(45) back_button.setStyleSheet(""" QPushButton { - background-color: #757575; + background-color: #95A5A6; color: white; border-radius: 5px; padding: 5px 15px; + font-size: 14px; + font-weight: bold; } QPushButton:hover { - background-color: #616161; + background-color: #7F8C8D; + } + QPushButton:pressed { + background-color: #616A6B; } """) back_button.clicked.connect(self.back_to_selection.emit) login_button = QPushButton("Login", self) - login_button.setMinimumHeight(40) - login_button.setFont(QFont("Arial", 12)) + login_button.setMinimumHeight(45) login_button.setStyleSheet(""" QPushButton { - background-color: #1E88E5; + background-color: #3498DB; color: white; border-radius: 5px; padding: 5px 15px; + font-size: 14px; + font-weight: bold; } QPushButton:hover { - background-color: #1976D2; + background-color: #2980B9; + } + QPushButton:pressed { + background-color: #1F618D; } """) login_button.clicked.connect(self.attempt_login) @@ -139,6 +204,12 @@ class LoginScreen(QWidget): # Add form to main layout layout.addWidget(form_container, 1) + # Footer + footer_label = QLabel(" 2025 Innovedus Inc. All rights reserved.", self) + footer_label.setAlignment(Qt.AlignCenter) + footer_label.setStyleSheet("font-size: 12px; color: #95A5A6;") + layout.addWidget(footer_label) + def attempt_login(self): username = self.username_input.text() password = self.password_input.text() @@ -149,7 +220,6 @@ class LoginScreen(QWidget): return # Simulate login success (in a real app, you would validate with your server) - # For demo, accept any non-empty username/password self.login_success.emit() def show_error(self, message): diff --git a/src/views/mainWindows.py b/src/views/mainWindows.py index 3db85c0..a642e9d 100644 --- a/src/views/mainWindows.py +++ b/src/views/mainWindows.py @@ -1,5 +1,5 @@ import os, sys, json, queue, numpy as np -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QGridLayout, QFrame, QMessageBox +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QGridLayout, QFrame, QMessageBox, QApplication, QListWidgetItem from PyQt5.QtCore import Qt, QTimer from PyQt5.QtGui import QPixmap, QMovie @@ -13,7 +13,7 @@ from src.views.components.canvas_area import create_canvas_area 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 +from src.views.components.device_popup import create_device_popup, refresh_devices class MainWindow(QWidget): def __init__(self): @@ -30,6 +30,7 @@ class MainWindow(QWidget): # Set up UI and configuration self.destination = None + self.current_bounding_box = None # 儲存當前的邊界框資訊 self.generate_global_config() self.init_ui() @@ -86,12 +87,19 @@ class MainWindow(QWidget): # 2. Set up popup and mask self.device_popup_mask_setup() + print("Setting up popup mask") - # 3. Refresh devices - self.device_controller.refresh_devices() + # 3. Refresh devices - do this after UI is fully set up + QTimer.singleShot(100, self.device_controller.refresh_devices) - # 4. Show popup + self.auto_start_camera() + + # # 4. Show popup self.show_device_popup() + print("Popup window setup complete") + + # # 5. Start camera automatically after a short delay + # QTimer.singleShot(100, self.auto_start_camera) except Exception as e: print(f"Error in show_device_popup_and_main_page: {e}") @@ -167,10 +175,16 @@ 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() except Exception as e: print(f"Error in show_device_popup: {e}") - + def hide_device_popup(self): try: self.overlay.hide() @@ -188,32 +202,57 @@ class MainWindow(QWidget): print(f"Error in clear_layout: {e}") def handle_inference_result(self, result): - """Handle inference results""" - # Print result to console - print("Inference result received:", result) - - # Create QMessageBox - msgBox = QMessageBox(self) - msgBox.setWindowTitle("Inference Result") - - # Format text based on result type - if isinstance(result, dict): - result_str = "\n".join(f"{key}: {value}" for key, value in result.items()) - else: - result_str = str(result) - - msgBox.setText(result_str) - msgBox.setStandardButtons(QMessageBox.Ok) - - # Set style - msgBox.setStyleSheet(""" - QLabel { - color: white; - } - """) - - msgBox.exec_() - + """處理來自推論工作器的結果""" + try: + # 輸出結果到控制台 + print("收到推論結果:", result) + + # 檢查結果是否包含邊界框資訊 + if isinstance(result, dict) and "bounding box" in result: + # 更新當前邊界框資訊 + self.current_bounding_box = result["bounding box"] + + # 如果結果中有標籤,將其添加到邊界框中 + if "result" in result: + # 將標籤添加為邊界框列表的第5個元素 + if isinstance(self.current_bounding_box, list) and len(self.current_bounding_box) >= 4: + # 確保邊界框列表至少有5個元素 + while len(self.current_bounding_box) < 5: + self.current_bounding_box.append(None) + # 設置第5個元素為結果標籤 + self.current_bounding_box[4] = result["result"] + + # 不需要顯示彈窗,因為邊界框會直接繪製在畫面上 + return + + # 對於非邊界框結果,使用彈窗顯示 + # 創建QMessageBox + msgBox = QMessageBox(self) + msgBox.setWindowTitle("推論結果") + + # 根據結果類型格式化文字 + if isinstance(result, dict): + result_str = "\n".join(f"{key}: {value}" for key, value in result.items()) + else: + result_str = str(result) + + msgBox.setText(result_str) + msgBox.setStandardButtons(QMessageBox.Ok) + + # 設置樣式 + msgBox.setStyleSheet(""" + QLabel { + color: white; + } + """) + + # 顯示彈窗 + msgBox.exec_() + except Exception as e: + print(f"處理推論結果時發生錯誤: {e}") + import traceback + print(traceback.format_exc()) + def show_no_device_gif(self): """Show a GIF indicating no devices are connected""" try: @@ -249,4 +288,23 @@ class MainWindow(QWidget): def generate_global_config(self): """Generate global configuration if needed""" - return self.config_utils.generate_global_config() \ No newline at end of file + return self.config_utils.generate_global_config() + + def auto_start_camera(self): + """自動啟動相機並顯示預覽""" + print("開始啟動相機...") + try: + # 先清除畫布,顯示加載中的信息 + if hasattr(self, 'canvas_label'): + self.canvas_label.setText("相機啟動中...") + self.canvas_label.setAlignment(Qt.AlignCenter) + self.canvas_label.setStyleSheet("color: white; font-size: 24px;") + # 確保UI更新 + QApplication.processEvents() + + # 在單獨的線程中啟動相機,避免阻塞UI + self.media_controller.start_camera() + except Exception as e: + print(f"啟動相機時發生錯誤: {e}") + import traceback + print(traceback.format_exc()) \ No newline at end of file diff --git a/src/views/selection_screen.py b/src/views/selection_screen.py index f9774ec..34f0882 100644 --- a/src/views/selection_screen.py +++ b/src/views/selection_screen.py @@ -1,8 +1,8 @@ -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame from PyQt5.QtCore import Qt, pyqtSignal -from PyQt5.QtGui import QPixmap, QFont +from PyQt5.QtGui import QPixmap, QFont, QIcon import os -from src.config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR +from src.config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR, APP_NAME class SelectionScreen(QWidget): # Signals for navigation @@ -16,77 +16,174 @@ class SelectionScreen(QWidget): def init_ui(self): # Basic window setup self.setGeometry(100, 100, *WINDOW_SIZE) - self.setStyleSheet(f"background-color: {BACKGROUND_COLOR};") + self.setStyleSheet(f"background-color: #F8F9FA;") # Slightly lighter background # Main layout layout = QVBoxLayout(self) + layout.setContentsMargins(40, 40, 40, 40) + layout.setSpacing(20) + + # Header with logo + header_frame = QFrame(self) + header_frame.setStyleSheet("background-color: #34495E; border-radius: 10px;") # Softer dark blue + header_frame.setFixedHeight(100) + + header_layout = QHBoxLayout(header_frame) + header_layout.setContentsMargins(20, 0, 20, 0) # Logo logo_label = QLabel(self) logo_path = os.path.join(UXUI_ASSETS, "Assets_png/kneron_logo.png") if os.path.exists(logo_path): logo_pixmap = QPixmap(logo_path) - logo_label.setPixmap(logo_pixmap) + scaled_logo = logo_pixmap.scaled(150, 60, Qt.KeepAspectRatio, Qt.SmoothTransformation) + logo_label.setPixmap(scaled_logo) logo_label.setAlignment(Qt.AlignCenter) - layout.addWidget(logo_label) + + header_layout.addWidget(logo_label, alignment=Qt.AlignCenter) + layout.addWidget(header_frame) + + # Content container + content_container = QFrame(self) + content_container.setStyleSheet(""" + QFrame { + background-color: white; + border-radius: 10px; + border: 1px solid #E0E0E0; + } + """) + content_layout = QVBoxLayout(content_container) + content_layout.setContentsMargins(30, 30, 30, 30) + content_layout.setSpacing(20) # Title - title_label = QLabel("Kneron Academy", self) + title_label = QLabel(APP_NAME, self) title_label.setAlignment(Qt.AlignCenter) - title_label.setFont(QFont("Arial", 24, QFont.Bold)) - layout.addWidget(title_label) + title_label.setStyleSheet("font-size: 28px; font-weight: bold; color: #34495E;") + content_layout.addWidget(title_label) # Subtitle - subtitle_label = QLabel("Please select an option to continue", self) + subtitle_label = QLabel("請選擇您要使用的功能", self) subtitle_label.setAlignment(Qt.AlignCenter) - subtitle_label.setFont(QFont("Arial", 14)) - layout.addWidget(subtitle_label) + subtitle_label.setStyleSheet("font-size: 16px; color: #7F8C8D;") + content_layout.addWidget(subtitle_label) # Add some space - layout.addSpacing(40) + content_layout.addSpacing(20) # Button container button_container = QWidget(self) button_layout = QHBoxLayout(button_container) - button_layout.setContentsMargins(50, 0, 50, 0) + button_layout.setContentsMargins(20, 0, 20, 0) + button_layout.setSpacing(40) # Increased spacing between buttons - # Utilities button - utilities_button = QPushButton("Utilities", self) - utilities_button.setMinimumHeight(80) - utilities_button.setFont(QFont("Arial", 14)) - utilities_button.setStyleSheet(""" - QPushButton { - background-color: #1E88E5; - color: white; - border-radius: 8px; - padding: 10px; + # Create card-style buttons with icons and descriptions + + # Utilities card button + utilities_card = QFrame(self) + utilities_card.setMinimumSize(300, 250) + utilities_card.setCursor(Qt.PointingHandCursor) + utilities_card.setStyleSheet(""" + QFrame { + background-color: white; + border-radius: 15px; + border: 1px solid #E0E0E0; } - QPushButton:hover { - background-color: #1976D2; + QFrame:hover { + background-color: #F5F9FF; + border: 1px solid #5DADE2; } """) - utilities_button.clicked.connect(self.open_utilities.emit) - # Demo App button - demo_button = QPushButton("Demo App", self) - demo_button.setMinimumHeight(80) - demo_button.setFont(QFont("Arial", 14)) - demo_button.setStyleSheet(""" - QPushButton { - background-color: #43A047; - color: white; - border-radius: 8px; - padding: 10px; + utilities_layout = QVBoxLayout(utilities_card) + utilities_layout.setContentsMargins(20, 20, 20, 20) + utilities_layout.setSpacing(15) + utilities_layout.setAlignment(Qt.AlignCenter) + + # Icon for utilities + utilities_icon_label = QLabel() + utilities_icon_path = os.path.join(UXUI_ASSETS, "Assets_svg/ic_dialog_device.svg") + if os.path.exists(utilities_icon_path): + utilities_icon = QPixmap(utilities_icon_path) + scaled_icon = utilities_icon.scaled(64, 64, Qt.KeepAspectRatio, Qt.SmoothTransformation) + utilities_icon_label.setPixmap(scaled_icon) + utilities_icon_label.setAlignment(Qt.AlignCenter) + utilities_layout.addWidget(utilities_icon_label, alignment=Qt.AlignCenter) + + # Title for utilities + utilities_title = QLabel("設備管理工具", self) + utilities_title.setAlignment(Qt.AlignCenter) + utilities_title.setStyleSheet("font-size: 22px; font-weight: bold; color: #34495E;") + utilities_layout.addWidget(utilities_title) + + # Description for utilities + utilities_desc = QLabel("連接、管理和更新您的 Kneron 設備", self) + utilities_desc.setAlignment(Qt.AlignCenter) + utilities_desc.setWordWrap(True) + utilities_desc.setStyleSheet("font-size: 14px; color: #7F8C8D;") + utilities_layout.addWidget(utilities_desc) + + # Connect the card to the signal + utilities_card.mousePressEvent = lambda event: self.open_utilities.emit() + + # Demo App card button + demo_card = QFrame(self) + demo_card.setMinimumSize(300, 250) + demo_card.setCursor(Qt.PointingHandCursor) + demo_card.setStyleSheet(""" + QFrame { + background-color: white; + border-radius: 15px; + border: 1px solid #E0E0E0; } - QPushButton:hover { - background-color: #388E3C; + QFrame:hover { + background-color: #F5FFF7; + border: 1px solid #7DCEA0; } """) - demo_button.clicked.connect(self.open_demo_app.emit) - # Add buttons to layout - button_layout.addWidget(utilities_button) - button_layout.addWidget(demo_button) + demo_layout = QVBoxLayout(demo_card) + demo_layout.setContentsMargins(20, 20, 20, 20) + demo_layout.setSpacing(15) + demo_layout.setAlignment(Qt.AlignCenter) - layout.addWidget(button_container) - layout.addStretch(1) # Push everything up + # Icon for demo app + demo_icon_label = QLabel() + demo_icon_path = os.path.join(UXUI_ASSETS, "Assets_svg/ic_recording_camera.svg") + if os.path.exists(demo_icon_path): + demo_icon = QPixmap(demo_icon_path) + scaled_icon = demo_icon.scaled(64, 64, Qt.KeepAspectRatio, Qt.SmoothTransformation) + demo_icon_label.setPixmap(scaled_icon) + demo_icon_label.setAlignment(Qt.AlignCenter) + demo_layout.addWidget(demo_icon_label, alignment=Qt.AlignCenter) + + # Title for demo app + demo_title = QLabel("AI 演示應用", self) + demo_title.setAlignment(Qt.AlignCenter) + demo_title.setStyleSheet("font-size: 22px; font-weight: bold; color: #34495E;") + demo_layout.addWidget(demo_title) + + # Description for demo app + demo_desc = QLabel("使用 AI 工具箱探索和測試 Kneron 設備的功能", self) + demo_desc.setAlignment(Qt.AlignCenter) + demo_desc.setWordWrap(True) + demo_desc.setStyleSheet("font-size: 14px; color: #7F8C8D;") + demo_layout.addWidget(demo_desc) + + # Connect the card to the signal + demo_card.mousePressEvent = lambda event: self.open_demo_app.emit() + + # Add cards to layout + button_layout.addWidget(utilities_card) + button_layout.addWidget(demo_card) + + content_layout.addWidget(button_container) + + # Add content container to main layout + layout.addWidget(content_container, 1) + + # Footer + footer_label = QLabel(" 2025 Innovedus Inc. All rights reserved.", self) + footer_label.setAlignment(Qt.AlignCenter) + footer_label.setStyleSheet("font-size: 12px; color: #95A5A6;") + layout.addWidget(footer_label) diff --git a/src/views/utilities_screen.py b/src/views/utilities_screen.py index e161fcd..7bd316c 100644 --- a/src/views/utilities_screen.py +++ b/src/views/utilities_screen.py @@ -1,11 +1,13 @@ from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame, QMessageBox, QScrollArea, QTableWidget, QTableWidgetItem, - QHeaderView, QProgressBar) + QHeaderView, QProgressBar, QLineEdit, QAbstractItemView) from PyQt5.QtCore import Qt, pyqtSignal, QTimer -from PyQt5.QtGui import QPixmap, QFont, QIcon +from PyQt5.QtGui import QPixmap, QFont, QIcon, QColor import os -from src.config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR +import kp +from src.config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR, DongleModelMap from src.controllers.device_controller import DeviceController +from src.services.device_service import check_available_device class UtilitiesScreen(QWidget): # Signals for navigation @@ -14,18 +16,68 @@ class UtilitiesScreen(QWidget): def __init__(self, parent=None): super().__init__(parent) self.device_controller = DeviceController(self) + self.current_page = "utilities" # 追蹤當前頁面: "utilities" 或 "purchased_items" self.init_ui() def init_ui(self): # Basic window setup self.setGeometry(100, 100, *WINDOW_SIZE) - self.setStyleSheet(f"background-color: {BACKGROUND_COLOR};") + self.setStyleSheet(f"background-color: #F5F7FA;") # Light gray background # Main layout - main_layout = QVBoxLayout(self) + self.main_layout = QVBoxLayout(self) + self.main_layout.setContentsMargins(20, 20, 20, 20) + self.main_layout.setSpacing(20) # Header with back button and logo - header_layout = QHBoxLayout() + header_frame = self.create_header() + self.main_layout.addWidget(header_frame) + + # 創建主要內容容器 + self.content_container = QFrame(self) + self.content_container.setStyleSheet(""" + QFrame { + background-color: white; + border-radius: 10px; + border: 1px solid #E0E0E0; + } + """) + content_layout = QVBoxLayout(self.content_container) + content_layout.setContentsMargins(20, 20, 20, 20) + content_layout.setSpacing(20) + + # 創建兩個頁面的容器 + self.utilities_page = QWidget() + self.purchased_items_page = QWidget() + + # 設置 utilities 頁面 + self.setup_utilities_page() + + # 設置 purchased items 頁面 + self.setup_purchased_items_page() + + # 添加頁面到內容容器 + content_layout.addWidget(self.utilities_page) + content_layout.addWidget(self.purchased_items_page) + + # 初始顯示 utilities 頁面 + self.utilities_page.show() + self.purchased_items_page.hide() + + # 添加內容容器到主佈局 + self.main_layout.addWidget(self.content_container, 1) + + # Initialize with device refresh + QTimer.singleShot(500, self.refresh_devices) + + def create_header(self): + """Create the header with back button and logo""" + header_frame = QFrame(self) + header_frame.setStyleSheet("background-color: #2C3E50; border-radius: 0px;") + header_frame.setFixedHeight(60) + + header_layout = QHBoxLayout(header_frame) + header_layout.setContentsMargins(20, 0, 20, 0) # Back button back_button = QPushButton("", self) @@ -38,11 +90,69 @@ class UtilitiesScreen(QWidget): border: none; } QPushButton:hover { - background-color: rgba(200, 200, 200, 0.3); + background-color: rgba(255, 255, 255, 0.1); border-radius: 20px; } """) back_button.clicked.connect(self.back_to_selection.emit) + header_layout.addWidget(back_button, alignment=Qt.AlignLeft) + + # Title + self.title_label = QLabel("Utilities", self) + self.title_label.setStyleSheet("color: white; font-size: 24px; font-weight: bold;") + header_layout.addWidget(self.title_label, alignment=Qt.AlignCenter) + + # Navigation buttons + nav_container = QFrame() + nav_layout = QHBoxLayout(nav_container) + nav_layout.setContentsMargins(0, 0, 0, 0) + nav_layout.setSpacing(10) + + # Utilities button + self.utilities_button = QPushButton("Utilities", self) + self.utilities_button.setStyleSheet(""" + QPushButton { + background-color: #3498DB; + color: white; + border: none; + border-radius: 5px; + padding: 5px 10px; + font-weight: bold; + } + QPushButton:hover { + background-color: #2980B9; + } + QPushButton:disabled { + background-color: #3498DB; + color: white; + } + """) + self.utilities_button.clicked.connect(self.show_utilities_page) + nav_layout.addWidget(self.utilities_button) + + # Purchased Items button + self.purchased_items_button = QPushButton("Purchased Items", self) + self.purchased_items_button.setStyleSheet(""" + QPushButton { + background-color: transparent; + color: #BDC3C7; + border: none; + border-radius: 5px; + padding: 5px 10px; + font-weight: bold; + } + QPushButton:hover { + color: white; + } + QPushButton:disabled { + background-color: #3498DB; + color: white; + } + """) + self.purchased_items_button.clicked.connect(self.show_purchased_items_page) + nav_layout.addWidget(self.purchased_items_button) + + header_layout.addWidget(nav_container) # Logo logo_label = QLabel(self) @@ -51,170 +161,529 @@ class UtilitiesScreen(QWidget): logo_pixmap = QPixmap(logo_path) scaled_logo = logo_pixmap.scaled(104, 40, Qt.KeepAspectRatio, Qt.SmoothTransformation) logo_label.setPixmap(scaled_logo) + header_layout.addWidget(logo_label, alignment=Qt.AlignRight) - header_layout.addWidget(back_button) - header_layout.addStretch(1) - header_layout.addWidget(logo_label) - - main_layout.addLayout(header_layout) - - # Title - title_label = QLabel("Utilities", self) - title_label.setAlignment(Qt.AlignCenter) - title_label.setFont(QFont("Arial", 24, QFont.Bold)) - main_layout.addWidget(title_label) - - # Create main content container - content_container = QFrame(self) - content_container.setFrameShape(QFrame.StyledPanel) - content_container.setStyleSheet(""" - QFrame { - background-color: white; - border-radius: 10px; - } - """) - content_layout = QVBoxLayout(content_container) + return header_frame + + def setup_utilities_page(self): + """Create the utilities page""" + utilities_layout = QVBoxLayout(self.utilities_page) + utilities_layout.setContentsMargins(0, 0, 0, 0) + utilities_layout.setSpacing(20) # Device connection section + device_section = self.create_device_section() + utilities_layout.addWidget(device_section) + + # Status section + status_section = self.create_status_section() + utilities_layout.addWidget(status_section) + + def setup_purchased_items_page(self): + """Create the purchased items page""" + purchased_items_layout = QVBoxLayout(self.purchased_items_page) + purchased_items_layout.setContentsMargins(0, 0, 0, 0) + purchased_items_layout.setSpacing(20) + + # 已購買項目區域 + purchased_items_section = self.create_purchased_items_section() + purchased_items_layout.addWidget(purchased_items_section) + + def create_purchased_items_section(self): + """創建已購買項目區域""" + purchased_section = QFrame() + purchased_section.setStyleSheet(""" + QFrame { + background-color: white; + border-radius: 8px; + border: 1px solid #E0E0E0; + } + """) + + purchased_layout = QVBoxLayout(purchased_section) + purchased_layout.setContentsMargins(15, 15, 15, 15) + purchased_layout.setSpacing(15) + + # 標題 + title_label = QLabel("Your Purchased Items", purchased_section) + title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #2C3E50;") + purchased_layout.addWidget(title_label) + + # 描述 + desc_label = QLabel("Select items to download to your device", purchased_section) + desc_label.setStyleSheet("font-size: 14px; color: #7F8C8D;") + purchased_layout.addWidget(desc_label) + + # 項目表格 + self.purchased_table = QTableWidget() + # 修改為只有5列,移除 "Action" 列 + self.purchased_table.setColumnCount(5) + self.purchased_table.setHorizontalHeaderLabels([ + "Select", "Product", "Model", "Current Version", "Compatible Dongles" + ]) + + # 設置行寬 + header = self.purchased_table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # 勾選框列 + header.setSectionResizeMode(1, QHeaderView.Stretch) + header.setSectionResizeMode(2, QHeaderView.Stretch) + header.setSectionResizeMode(3, QHeaderView.ResizeToContents) + header.setSectionResizeMode(4, QHeaderView.Stretch) + + # 設置表格高度 + self.purchased_table.setMinimumHeight(300) + + # 啟用整行選擇 + self.purchased_table.setSelectionBehavior(QAbstractItemView.SelectRows) + + self.purchased_table.setStyleSheet(""" + QTableWidget { + background-color: white; + color: #2C3E50; + border: 1px solid #E0E0E0; + border-radius: 8px; + gridline-color: #E0E0E0; + } + QTableWidget::item { + padding: 8px; + border-bottom: 1px solid #E0E0E0; + } + QTableWidget::item:selected { + background-color: #3498DB; + color: white; + } + QHeaderView::section { + background-color: #3498DB; + color: white; + padding: 8px; + border: none; + font-weight: bold; + } + """) + purchased_layout.addWidget(self.purchased_table) + + # 添加一些模擬數據 + self.populate_mock_purchased_items() + + # 下載按鈕 + button_layout = QHBoxLayout() + button_layout.setSpacing(10) + + refresh_button = QPushButton("Refresh Items", purchased_section) + refresh_button.setMinimumHeight(40) + refresh_button.setStyleSheet(""" + QPushButton { + background-color: #3498DB; + color: white; + border: none; + border-radius: 5px; + padding: 10px 15px; + font-weight: bold; + font-size: 14px; + } + QPushButton:hover { + background-color: #2980B9; + } + QPushButton:pressed { + background-color: #1F618D; + } + """) + refresh_button.clicked.connect(self.populate_mock_purchased_items) + button_layout.addWidget(refresh_button) + + download_button = QPushButton("Download Selected Items", purchased_section) + download_button.setMinimumHeight(40) + download_button.setStyleSheet(""" + QPushButton { + background-color: #2ECC71; + color: white; + border: none; + border-radius: 5px; + padding: 10px 15px; + font-weight: bold; + font-size: 14px; + } + QPushButton:hover { + background-color: #27AE60; + } + QPushButton:pressed { + background-color: #1E8449; + } + """) + download_button.clicked.connect(self.download_selected_items) + button_layout.addWidget(download_button) + + purchased_layout.addLayout(button_layout) + + return purchased_section + + def populate_mock_purchased_items(self): + """填充模擬的已購買項目數據""" + # 清空表格 + self.purchased_table.setRowCount(0) + + # 模擬數據 + mock_items = [ + { + "product": "KL720 AI Package", + "model": "Face Detection", + "version": "1.2.3", + "dongles": "KL720, KL730" + }, + { + "product": "KL520 AI Package", + "model": "Object Detection", + "version": "2.0.1", + "dongles": "KL520" + }, + { + "product": "KL720 AI Package", + "model": "Pose Estimation", + "version": "1.5.0", + "dongles": "KL720, KL730, KL830" + }, + { + "product": "KL630 AI Package", + "model": "Image Classification", + "version": "3.1.2", + "dongles": "KL630, KL720" + }, + { + "product": "KL830 AI Package", + "model": "Semantic Segmentation", + "version": "1.0.0", + "dongles": "KL830" + } + ] + + # 添加數據到表格 + for i, item in enumerate(mock_items): + self.purchased_table.insertRow(i) + + # 創建勾選框 + checkbox_item = QTableWidgetItem() + checkbox_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsSelectable) + checkbox_item.setCheckState(Qt.Unchecked) + self.purchased_table.setItem(i, 0, checkbox_item) + + self.purchased_table.setItem(i, 1, QTableWidgetItem(item["product"])) + self.purchased_table.setItem(i, 2, QTableWidgetItem(item["model"])) + self.purchased_table.setItem(i, 3, QTableWidgetItem(item["version"])) + self.purchased_table.setItem(i, 4, QTableWidgetItem(item["dongles"])) + + def download_item(self, row): + """下載特定項目""" + product = self.purchased_table.item(row, 1).text() + model = self.purchased_table.item(row, 2).text() + + # 顯示進度條 + self.show_progress(f"Downloading {product} - {model}...", 0) + + # 模擬下載過程 + for i in range(1, 11): + progress = i * 10 + QTimer.singleShot(i * 300, lambda p=progress: self.update_progress(p)) + + # 完成下載 + QTimer.singleShot(3000, lambda: self.handle_download_complete(product, model)) + + def handle_download_complete(self, product, model): + """處理下載完成""" + self.hide_progress() + QMessageBox.information(self, "Download Complete", f"{product} - {model} has been downloaded successfully!") + + def download_selected_items(self): + """下載所有勾選的項目""" + selected_rows = set() + + # 檢查勾選的項目 + for row in range(self.purchased_table.rowCount()): + if self.purchased_table.item(row, 0).checkState() == Qt.Checked: + selected_rows.add(row) + + if not selected_rows: + QMessageBox.warning(self, "No Selection", "Please select at least one item to download") + return + + # 顯示進度條 + self.show_progress(f"Downloading {len(selected_rows)} items...", 0) + + # 模擬下載過程 + total_items = len(selected_rows) + for i, row in enumerate(selected_rows): + product = self.purchased_table.item(row, 1).text() + model = self.purchased_table.item(row, 2).text() + progress = int((i / total_items) * 100) + + # 更新進度條 + self.update_progress(progress) + self.progress_title.setText(f"Downloading {product} - {model}... ({i+1}/{total_items})") + + # 模擬下載延遲 + QTimer.singleShot((i+1) * 1000, lambda p=product, m=model: self.status_label.setText(f"Downloaded {p} - {m}")) + + # 完成所有下載 + QTimer.singleShot((total_items+1) * 1000, self.handle_all_downloads_complete) + + def update_download_progress(self, progress, message): + """更新下載進度""" + self.update_progress(progress) + self.progress_title.setText(message) + + def handle_all_downloads_complete(self): + """處理所有下載完成""" + self.hide_progress() + QMessageBox.information(self, "Downloads Complete", "All selected items have been downloaded successfully!") + + def create_device_section(self): + """Create the device connection section""" device_section = QFrame(self) device_section.setStyleSheet(""" QFrame { - background-color: #f5f5f5; + background-color: white; border-radius: 8px; - padding: 10px; + border: 1px solid #E0E0E0; } """) + device_layout = QVBoxLayout(device_section) + device_layout.setContentsMargins(15, 15, 15, 15) + device_layout.setSpacing(15) - device_title = QLabel("Device Connection", self) - device_title.setFont(QFont("Arial", 16, QFont.Bold)) - device_layout.addWidget(device_title) + # Title + title_label = QLabel("Device Connection", self) + title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #2C3E50;") + device_layout.addWidget(title_label) - device_subtitle = QLabel("Connect and manage your Kneron devices", self) - device_subtitle.setFont(QFont("Arial", 12)) - device_layout.addWidget(device_subtitle) + # Description + desc_label = QLabel("Connect and manage your Kneron devices", self) + desc_label.setStyleSheet("font-size: 14px; color: #7F8C8D;") + device_layout.addWidget(desc_label) - # Device table - self.device_table = QTableWidget(0, 5, self) + # Create device table + self.device_table = QTableWidget() + self.device_table.setColumnCount(6) self.device_table.setHorizontalHeaderLabels([ - "Device ID", "Product ID", "Firmware", "KN Number", "Status" + "Device Type", "Port ID", "Firmware Version", "KN Number", "Link Speed", "Status" ]) - self.device_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + + # Enable row selection mode + self.device_table.setSelectionBehavior(QTableWidget.SelectRows) + self.device_table.setSelectionMode(QTableWidget.SingleSelection) + self.device_table.setStyleSheet(""" QTableWidget { - border: none; - gridline-color: #e0e0e0; + background-color: white; + color: #2C3E50; + border: 1px solid #E0E0E0; + border-radius: 8px; + gridline-color: #E0E0E0; + } + QTableWidget::item { + padding: 8px; + border-bottom: 1px solid #E0E0E0; + } + QTableWidget::item:selected { + background-color: #3498DB; + color: white; } QHeaderView::section { - background-color: #f0f0f0; + background-color: #3498DB; + color: white; padding: 8px; - font-weight: bold; border: none; - border-bottom: 1px solid #e0e0e0; + font-weight: bold; } """) - device_layout.addWidget(self.device_table) - # Refresh and actions buttons - device_buttons_layout = QHBoxLayout() + # Set header properties + header = self.device_table.horizontalHeader() + for i in range(6): + header.setSectionResizeMode(i, QHeaderView.Stretch) + # Add the table to a scroll area + table_scroll = QScrollArea() + table_scroll.setWidgetResizable(True) + table_scroll.setStyleSheet(""" + QScrollArea { + border: none; + background-color: transparent; + } + """) + table_scroll.setWidget(self.device_table) + device_layout.addWidget(table_scroll) + + # Connect selection changed signal + self.device_table.itemSelectionChanged.connect(self.on_device_selection_changed) + + # Button layout + button_layout = QHBoxLayout() + button_layout.setSpacing(10) + + # Refresh button refresh_button = QPushButton("Refresh Devices", self) + refresh_button.setMinimumHeight(40) refresh_button.setStyleSheet(""" QPushButton { - background-color: #42a5f5; + background-color: #3498DB; color: white; + border: 2px solid #2980B9; border-radius: 5px; - padding: 8px 15px; + padding: 10px 15px; + font-weight: bold; + font-size: 14px; } QPushButton:hover { - background-color: #2196f3; + background-color: #2980B9; + } + QPushButton:pressed { + background-color: #1F618D; } """) refresh_button.clicked.connect(self.refresh_devices) + button_layout.addWidget(refresh_button) + # Register button register_button = QPushButton("Register Device", self) + register_button.setMinimumHeight(40) register_button.setStyleSheet(""" QPushButton { - background-color: #66bb6a; + background-color: #2ECC71; color: white; + border: 2px solid #27AE60; border-radius: 5px; - padding: 8px 15px; + padding: 10px 15px; + font-weight: bold; + font-size: 14px; } QPushButton:hover { - background-color: #4caf50; + background-color: #27AE60; + } + QPushButton:pressed { + background-color: #1E8449; } """) register_button.clicked.connect(self.register_device) + button_layout.addWidget(register_button) - update_fw_button = QPushButton("Update Firmware", self) - update_fw_button.setStyleSheet(""" + # Update firmware button + update_button = QPushButton("Update Firmware", self) + update_button.setMinimumHeight(40) + update_button.setStyleSheet(""" QPushButton { - background-color: #ffa726; + background-color: #F39C12; color: white; + border: 2px solid #D35400; border-radius: 5px; - padding: 8px 15px; + padding: 10px 15px; + font-weight: bold; + font-size: 14px; } QPushButton:hover { - background-color: #ff9800; + background-color: #D35400; + } + QPushButton:pressed { + background-color: #A04000; } """) - update_fw_button.clicked.connect(self.update_firmware) + update_button.clicked.connect(self.update_firmware) + button_layout.addWidget(update_button) - device_buttons_layout.addWidget(refresh_button) - device_buttons_layout.addWidget(register_button) - device_buttons_layout.addWidget(update_fw_button) + # Install Driver button + install_driver_button = QPushButton("Install Driver", self) + install_driver_button.setMinimumHeight(40) + install_driver_button.setStyleSheet(""" + QPushButton { + background-color: #9B59B6; + color: white; + border: 2px solid #8E44AD; + border-radius: 5px; + padding: 10px 15px; + font-weight: bold; + font-size: 14px; + } + QPushButton:hover { + background-color: #8E44AD; + } + QPushButton:pressed { + background-color: #7D3C98; + } + """) + install_driver_button.clicked.connect(self.install_drivers) + button_layout.addWidget(install_driver_button) - device_layout.addLayout(device_buttons_layout) + device_layout.addLayout(button_layout) - # Add device section to content - content_layout.addWidget(device_section) - - # Status section + return device_section + + def create_status_section(self): + """Create the status section""" status_section = QFrame(self) status_section.setStyleSheet(""" QFrame { - background-color: #f5f5f5; + background-color: white; border-radius: 8px; - padding: 10px; - margin-top: 15px; + border: 1px solid #E0E0E0; } """) + status_layout = QVBoxLayout(status_section) + status_layout.setContentsMargins(15, 15, 15, 15) + status_layout.setSpacing(15) - status_title = QLabel("Device Status", self) - status_title.setFont(QFont("Arial", 16, QFont.Bold)) - status_layout.addWidget(status_title) + # Title + title_label = QLabel("Device Status", self) + title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #2C3E50;") + status_layout.addWidget(title_label) - # Current status - self.status_label = QLabel("No devices connected", self) - self.status_label.setFont(QFont("Arial", 12)) + # Status message + self.status_label = QLabel("No devices found", self) + self.status_label.setStyleSheet("font-size: 14px; color: #7F8C8D;") status_layout.addWidget(self.status_label) - # Progress bar for operations + # Progress section self.progress_section = QFrame(self) self.progress_section.setVisible(False) + self.progress_section.setStyleSheet(""" + QFrame { + background-color: #F8F9FA; + border-radius: 5px; + border: 1px solid #E0E0E0; + padding: 10px; + } + """) + progress_layout = QVBoxLayout(self.progress_section) + progress_layout.setContentsMargins(10, 10, 10, 10) + progress_layout.setSpacing(10) self.progress_title = QLabel("Operation in progress...", self) + self.progress_title.setStyleSheet("font-size: 14px; font-weight: bold; color: #2C3E50;") progress_layout.addWidget(self.progress_title) self.progress_bar = QProgressBar(self) self.progress_bar.setRange(0, 100) self.progress_bar.setValue(0) self.progress_bar.setTextVisible(True) + self.progress_bar.setStyleSheet(""" + QProgressBar { + border: 1px solid #E0E0E0; + border-radius: 5px; + background-color: white; + text-align: center; + height: 20px; + } + QProgressBar::chunk { + background-color: #3498DB; + border-radius: 5px; + } + """) progress_layout.addWidget(self.progress_bar) status_layout.addWidget(self.progress_section) - # Add status section to content - content_layout.addWidget(status_section) - - # Add the main content to the layout - main_layout.addWidget(content_container, 1) - - # Initialize with device refresh - QTimer.singleShot(500, self.refresh_devices) - + return status_section + def refresh_devices(self): """Refresh the list of devices""" try: @@ -225,111 +694,194 @@ class UtilitiesScreen(QWidget): self.show_progress("Scanning for devices...", 0) # Get the devices - device_descriptors = self.device_controller.get_devices() + device_descriptors = check_available_device() # Update progress self.update_progress(50) # Display the devices in the table - if hasattr(device_descriptors, 'device_descriptor_list'): + if device_descriptors.device_descriptor_number > 0: devices = device_descriptors.device_descriptor_list for i, device in enumerate(devices): self.device_table.insertRow(i) - # Device ID - usb_id = QTableWidgetItem(str(device.get("usb_port_id", "-"))) - self.device_table.setItem(i, 0, usb_id) + # Product ID to Device Model mapping + product_id_hex = hex(device.product_id) + # Map the product_id to device model name using DongleModelMap + device_model = DongleModelMap.get(product_id_hex, product_id_hex) + model_item = QTableWidgetItem(device_model) + self.device_table.setItem(i, 0, model_item) - # Product ID - product_id = QTableWidgetItem(str(device.get("product_id", "-"))) - self.device_table.setItem(i, 1, product_id) + # Device ID (Port ID) + port_id = str(device.usb_port_id) + usb_id = QTableWidgetItem(port_id) + self.device_table.setItem(i, 1, usb_id) + + # 嘗試獲取 system_info 中的 firmware_version + firmware_version = "-" + try: + if device.is_connectable: + # 連接設備並獲取系統信息 + device_group = kp.core.connect_devices(usb_port_ids=[port_id]) + system_info = kp.core.get_system_info( + device_group=device_group, + usb_port_id=device.usb_port_id + ) + # 從 system_info 對象獲取固件版本 + if system_info and hasattr(system_info, 'firmware_version'): + # firmware_version 是一個對象,獲取其字符串表示 + fw_version = system_info.firmware_version + if hasattr(fw_version, 'firmware_version'): + # 提取版本號,移除字典格式 + version_str = fw_version.firmware_version + # 如果版本是字典格式,提取其中的值 + if isinstance(version_str, dict) and 'firmware_version' in version_str: + firmware_version = version_str['firmware_version'] + else: + firmware_version = version_str + else: + # 將對象轉換為字符串並清理格式 + version_str = str(fw_version) + # 嘗試從字符串中提取版本號 + import re + match = re.search(r'"firmware_version":\s*"([^"]+)"', version_str) + if match: + firmware_version = match.group(1) + else: + firmware_version = version_str + except Exception as e: + print(f"Error getting firmware version: {e}") # Firmware - firmware = QTableWidgetItem(str(device.get("firmware", "-"))) + firmware = QTableWidgetItem(firmware_version) self.device_table.setItem(i, 2, firmware) # KN Number - kn_number = QTableWidgetItem(str(device.get("kn_number", "-"))) + kn_number = QTableWidgetItem(str(device.kn_number)) self.device_table.setItem(i, 3, kn_number) + # Link Speed + link_speed_str = "Unknown" + if hasattr(device, 'link_speed'): + # 從完整的 link_speed 字符串中提取 SPEED_XXX 部分 + full_speed = str(device.link_speed) + if "SUPER" in full_speed: + link_speed_str = "SUPER" + elif "HIGH" in full_speed: + link_speed_str = "HIGH" + elif "FULL" in full_speed: + link_speed_str = "FULL" + else: + # 嘗試提取 KP_USB_SPEED_XXX 部分 + import re + match = re.search(r'KP_USB_SPEED_(\w+)', full_speed) + if match: + link_speed_str = match.group(1) + + link_speed = QTableWidgetItem(link_speed_str) + self.device_table.setItem(i, 4, link_speed) + # Status - status = QTableWidgetItem("Connected" if device.get("is_connectable", False) else "Not Available") - self.device_table.setItem(i, 4, status) + status = QTableWidgetItem("Connected" if device.is_connectable else "Not Available") + status.setForeground(Qt.green if device.is_connectable else Qt.red) + self.device_table.setItem(i, 5, status) # Hide progress self.hide_progress() # Update status - if self.device_table.rowCount() > 0: - self.status_label.setText(f"Found {self.device_table.rowCount()} device(s)") + device_count = self.device_table.rowCount() + if device_count > 0: + self.status_label.setText(f"Found {device_count} device(s)") + self.status_label.setStyleSheet("font-size: 14px; color: #27AE60; font-weight: bold;") else: self.status_label.setText("No devices found") - + self.status_label.setStyleSheet("font-size: 14px; color: #E74C3C;") + + return device_count > 0 + except Exception as e: + print(f"Error refreshing devices: {e}") self.hide_progress() - self.status_label.setText(f"Error refreshing devices: {str(e)}") - QMessageBox.critical(self, "Error", f"Failed to scan for devices: {str(e)}") + self.status_label.setText(f"Error: {str(e)}") + self.status_label.setStyleSheet("font-size: 14px; color: #E74C3C;") + return False def register_device(self): """Register the selected device""" selected_rows = self.device_table.selectedItems() if not selected_rows: - QMessageBox.warning(self, "No Selection", "Please select a device to register") + QMessageBox.warning(self, "Warning", "Please select a device to register") return - row = selected_rows[0].row() - device_id = self.device_table.item(row, 0).text() - kn_number = self.device_table.item(row, 3).text() - - # Show confirmation dialog - reply = QMessageBox.question(self, "Register Device", - f"Do you want to register device with KN Number: {kn_number}?", - QMessageBox.Yes | QMessageBox.No, QMessageBox.No) - - if reply == QMessageBox.Yes: - # Show progress - self.show_progress("Registering device...", 0) - - # Simulate registration process - for i in range(1, 5): - QTimer.singleShot(i * 500, lambda val=i*25: self.update_progress(val)) - - # Simulate completion - QTimer.singleShot(2500, lambda: self.registration_complete(True)) + # In a real application, you would implement the device registration logic here + QMessageBox.information(self, "Info", "Device registration functionality will be implemented in a future update") def update_firmware(self): """Update firmware for the selected device""" selected_rows = self.device_table.selectedItems() if not selected_rows: - QMessageBox.warning(self, "No Selection", "Please select a device to update") + QMessageBox.warning(self, "Warning", "Please select a device to update firmware") return - row = selected_rows[0].row() - device_id = self.device_table.item(row, 0).text() - current_fw = self.device_table.item(row, 2).text() - - # Show confirmation dialog - reply = QMessageBox.question(self, "Update Firmware", - f"Current firmware: {current_fw}\nDo you want to update the firmware?", - QMessageBox.Yes | QMessageBox.No, QMessageBox.No) - - if reply == QMessageBox.Yes: - # Show progress - self.show_progress("Downloading firmware...", 0) + # In a real application, you would implement the firmware update logic here + QMessageBox.information(self, "Info", "Firmware update functionality will be implemented in a future update") + + def install_drivers(self): + """Install drivers for all supported Kneron devices""" + try: + # 顯示進度條 + self.show_progress("Installing Kneron Device Drivers...", 0) - # Simulate download process - for i in range(1, 5): - QTimer.singleShot(i * 500, lambda val=i*25: self.update_progress(val)) + # 列出所有產品 ID + product_ids = [ + kp.ProductId.KP_DEVICE_KL520, + kp.ProductId.KP_DEVICE_KL720_LEGACY, + kp.ProductId.KP_DEVICE_KL720, + kp.ProductId.KP_DEVICE_KL630, + kp.ProductId.KP_DEVICE_KL730, + kp.ProductId.KP_DEVICE_KL830 + ] - # Simulate installation - QTimer.singleShot(2500, lambda: self.show_progress("Installing firmware...", 50)) + success_count = 0 + total_count = len(product_ids) - for i in range(1, 5): - QTimer.singleShot(2500 + i * 500, lambda val=50+i*10: self.update_progress(val)) + # 安裝每個驅動程式 + for i, product_id in enumerate(product_ids): + try: + # 更新進度條 + progress = int((i / total_count) * 100) + self.update_progress(progress) + self.progress_title.setText(f"Installing [{product_id.name}] driver...") + + # 安裝驅動程式 + kp.core.install_driver_for_windows(product_id=product_id) + success_count += 1 + + # 更新狀態訊息 + self.status_label.setText(f"Successfully installed {product_id.name} driver") + except kp.ApiKPException as exception: + error_msg = f"Error: install {product_id.name} driver failed, error msg: [{str(exception)}]" + self.status_label.setText(error_msg) + QMessageBox.warning(self, "Driver Installation Error", error_msg) - # Simulate completion - QTimer.singleShot(5000, lambda: self.firmware_update_complete(True)) + # 完成安裝 + self.update_progress(100) + self.hide_progress() + + # 顯示結果訊息 + if success_count == total_count: + QMessageBox.information(self, "Success", "All Kneron device drivers installed successfully!") + else: + QMessageBox.information(self, "Partial Success", + f"Installed {success_count} out of {total_count} drivers. Check the status for details.") + + except Exception as e: + self.hide_progress() + error_msg = f"Error during driver installation: {str(e)}" + self.status_label.setText(error_msg) + QMessageBox.critical(self, "Error", error_msg) def show_progress(self, title, value): """Show the progress bar with the given title and value""" @@ -345,29 +897,97 @@ class UtilitiesScreen(QWidget): """Hide the progress section""" self.progress_section.setVisible(False) - def registration_complete(self, success): - """Handle registration completion""" - self.hide_progress() - - if success: - QMessageBox.information(self, "Registration Complete", "Device registration successful!") - # Update the status in the table - selected_row = self.device_table.selectedItems()[0].row() - self.device_table.setItem(selected_row, 4, QTableWidgetItem("Registered")) - else: - QMessageBox.critical(self, "Registration Failed", "Failed to register device. Please try again.") + def on_device_selection_changed(self): + """Handle device selection changes""" + selected_rows = self.device_table.selectionModel().selectedRows() + if selected_rows: + # Get the selected row index + row_index = selected_rows[0].row() + + # Update the status label to show which device is selected + device_model = self.device_table.item(row_index, 0).text() + device_id = self.device_table.item(row_index, 1).text() + self.status_label.setText(f"Selected device: {device_model} (ID: {device_id})") + + # Ensure the entire row is highlighted + self.device_table.selectRow(row_index) - def firmware_update_complete(self, success): - """Handle firmware update completion""" - self.hide_progress() - - if success: - QMessageBox.information(self, "Update Complete", "Firmware update successful!") - # Update the firmware version in the table (simulate a new version) - selected_row = self.device_table.selectedItems()[0].row() - current_fw = self.device_table.item(selected_row, 2).text() - if current_fw.endswith("F"): - new_fw = current_fw + " (Updated)" - self.device_table.setItem(selected_row, 2, QTableWidgetItem(new_fw)) - else: - QMessageBox.critical(self, "Update Failed", "Failed to update firmware. Please try again.") + def show_utilities_page(self): + self.utilities_button.setStyleSheet(""" + QPushButton { + background-color: #3498DB; + color: white; + border: none; + border-radius: 5px; + padding: 5px 10px; + font-weight: bold; + } + QPushButton:hover { + background-color: #2980B9; + } + QPushButton:disabled { + background-color: #3498DB; + color: white; + } + """) + self.purchased_items_button.setStyleSheet(""" + QPushButton { + background-color: transparent; + color: #BDC3C7; + border: none; + border-radius: 5px; + padding: 5px 10px; + font-weight: bold; + } + QPushButton:hover { + color: white; + } + QPushButton:disabled { + background-color: #3498DB; + color: white; + } + """) + self.utilities_page.show() + self.purchased_items_page.hide() + self.title_label.setText("Utilities") + self.current_page = "utilities" + + def show_purchased_items_page(self): + self.purchased_items_button.setStyleSheet(""" + QPushButton { + background-color: #3498DB; + color: white; + border: none; + border-radius: 5px; + padding: 5px 10px; + font-weight: bold; + } + QPushButton:hover { + background-color: #2980B9; + } + QPushButton:disabled { + background-color: #3498DB; + color: white; + } + """) + self.utilities_button.setStyleSheet(""" + QPushButton { + background-color: transparent; + color: #BDC3C7; + border: none; + border-radius: 5px; + padding: 5px 10px; + font-weight: bold; + } + QPushButton:hover { + color: white; + } + QPushButton:disabled { + background-color: #3498DB; + color: white; + } + """) + self.utilities_page.hide() + self.purchased_items_page.show() + self.title_label.setText("Purchased Items") + self.current_page = "purchased_items" diff --git a/update_diary.md b/update_diary.md index 1dcbd9d..d335940 100644 --- a/update_diary.md +++ b/update_diary.md @@ -32,4 +32,43 @@ Times: 8.5 Hours 2. 增加不同 input format for python script -> 目前是用 numpy 的方式 3. 修改 popup windows 中顯示的 dongle 型號 -> 使用類似 完成事項4 的方式使用之前的 mapping 4. 修改 inference output format including bb and single model -5. multi-dongle inference \ No newline at end of file +5. multi-dongle inference + + + +## 20250324 +1. 推論引擎優化 +幀尺寸不匹配問題修復 +修復了在暫停視頻模式後上傳圖片時出現的尺寸不匹配錯誤 +在 InferenceWorkerThread 的 run 方法中添加了幀尺寸檢查 +實現了比較當前幀與上一幀尺寸的機制,當尺寸不同時重置緩存 +解決了 "operands could not be broadcast together with shapes" 錯誤 +推論佇列管理優化 +添加了 _clear_inference_queue 方法,實現安全清空推論佇列 +在模式切換時清空佇列,避免使用舊數據 +在處理新上傳圖片前清空佇列 +重構了 process_uploaded_image 方法,提高可讀性和錯誤處理 +2. Utilities 畫面功能增強 +驅動程式安裝功能 +添加了紫色的「Install Driver」按鈕 +實現了 install_drivers 方法,支援多種 Kneron 裝置 +使用 kp.core.install_driver_for_windows API 安裝驅動程式 +添加了進度條顯示和詳細的錯誤處理 +Purchased Items 頁面改進 +移除了登入要求,讓用戶無需登入即可查看已購買項目 +修改了頁面佈局,使其在同一個視窗中顯示,而非彈出視窗 +添加了表格顯示已購買項目,包含產品、模型、版本和相容裝置資訊 +實現了「Refresh Items」按鈕功能 +表格功能增強 +添加了勾選框功能,允許用戶選擇多個項目 +實現了整行選擇功能,點擊任一單元格時整行變色 +修改了「Download Selected Items」按鈕功能,實現批量下載選中項目 +簡化了表格結構,移除了單獨的「Action」列和下載按鈕 +3. 用戶界面優化 +視覺設計改進 +統一了頁面風格和配色 +改進了按鈕和表格的樣式 +優化了佈局結構,提供更一致的用戶體驗 +導航優化 +實現了 Utilities 和 Purchased Items 頁面之間的無縫切換 +改進了頁面標題和描述,提供更清晰的功能說明 \ No newline at end of file