diff --git a/main.py b/main.py index 0de4f15..8c79da3 100644 --- a/main.py +++ b/main.py @@ -1,13 +1,75 @@ import sys -from PyQt5.QtWidgets import QApplication +from PyQt5.QtWidgets import QApplication, QStackedWidget from src.views.mainWindows import MainWindow +from src.views.selection_screen import SelectionScreen +from src.views.login_screen import LoginScreen +from src.views.utilities_screen import UtilitiesScreen from src.config import APP_NAME, WINDOW_SIZE +class AppController: + def __init__(self): + self.app = QApplication(sys.argv) + self.stack = QStackedWidget() + self.stack.setGeometry(100, 100, *WINDOW_SIZE) + self.stack.setWindowTitle(APP_NAME) + + # Initialize screens + self.init_screens() + + # Configure navigation signals + self.connect_signals() + + # Start with selection screen + self.show_selection_screen() + + def init_screens(self): + # Selection screen + self.selection_screen = SelectionScreen() + self.stack.addWidget(self.selection_screen) + + # Login screen + self.login_screen = LoginScreen() + self.stack.addWidget(self.login_screen) + + # Utilities screen + self.utilities_screen = UtilitiesScreen() + self.stack.addWidget(self.utilities_screen) + + # Demo app (main window) + self.main_window = MainWindow() + self.stack.addWidget(self.main_window) + + def connect_signals(self): + # Selection screen signals + self.selection_screen.open_utilities.connect(self.show_login_screen) + self.selection_screen.open_demo_app.connect(self.show_demo_app) + + # Login screen signals + self.login_screen.login_success.connect(self.show_utilities_screen) + self.login_screen.back_to_selection.connect(self.show_selection_screen) + + # Utilities screen signals + self.utilities_screen.back_to_selection.connect(self.show_selection_screen) + + def show_selection_screen(self): + self.stack.setCurrentWidget(self.selection_screen) + + def show_login_screen(self): + self.stack.setCurrentWidget(self.login_screen) + + def show_utilities_screen(self): + self.stack.setCurrentWidget(self.utilities_screen) + + def show_demo_app(self): + self.stack.setCurrentWidget(self.main_window) + + def run(self): + self.stack.show() + return self.app.exec_() + def main(): - app = QApplication(sys.argv) - window = MainWindow() - window.show() - sys.exit(app.exec_()) + controller = AppController() + return controller.run() if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/config.py b/src/config.py index b3eae69..c020fc6 100644 --- a/src/config.py +++ b/src/config.py @@ -1,6 +1,7 @@ from enum import Enum import os -APPDATA_PATH = os.environ.get("LOCALAPPDATA") +# 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 8c9213e..0ba6611 100644 --- a/src/controllers/device_controller.py +++ b/src/controllers/device_controller.py @@ -1,32 +1,75 @@ -import kp -from typing import List, Dict +# src/controllers/device_controller.py +from PyQt5.QtWidgets import QWidget, QListWidgetItem +from PyQt5.QtGui import QPixmap +from PyQt5.QtCore import Qt +import os + +from src.services.device_service import check_available_device +from src.config import UXUI_ASSETS, DongleModelMap, DongleIconMap class DeviceController: - def __init__(self): + def __init__(self, main_window): + self.main_window = main_window + self.selected_device = None self.connected_devices = [] - def scan_devices(self): - return kp.core.scan_devices() - - def connect_device(self, usb_port_id: int): - device_group = kp.core.connect_devices(usb_port_ids=[usb_port_id]) - kp.core.set_timeout(device_group=device_group, milliseconds=5000) - return device_group + def refresh_devices(self): + """Refresh the list of connected devices""" + try: + print("Refreshing devices...") + device_descriptors = check_available_device() + self.connected_devices = [] - def load_firmware(self, device_group, product_id: int): - SCPU_FW_PATH = f'../../external/res/firmware/{product_id}/fw_scpu.bin' - NCPU_FW_PATH = f'../../external/res/firmware/{product_id}/fw_ncpu.bin' - kp.core.load_firmware_from_file( - device_group=device_group, - scpu_fw_path=SCPU_FW_PATH, - ncpu_fw_path=NCPU_FW_PATH - ) + if device_descriptors.device_descriptor_number > 0: + self.parse_and_store_devices(device_descriptors.device_descriptor_list) + self.display_devices(device_descriptors.device_descriptor_list) + return True + else: + self.main_window.show_no_device_gif() + return False + except Exception as e: + print(f"Error in refresh_devices: {e}") + return False - def parse_device_info(self, device) -> Dict: - return { - 'usb_port_id': device.usb_port_id, - 'product_id': device.product_id, - 'kn_number': device.kn_number - } + 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 + + 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) + + def get_selected_device(self): + """Get the currently selected device""" + return self.selected_device + + def select_device(self, device, list_item, list_widget): + """Select a device and update UI""" + self.selected_device = device + print("Selected dongle:", device) - \ No newline at end of file + # Update list item visual selection + for index in range(list_widget.count()): + item = list_widget.item(index) + widget = list_widget.itemWidget(item) + 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 diff --git a/src/controllers/inference_controller.py b/src/controllers/inference_controller.py new file mode 100644 index 0000000..c5c3a7d --- /dev/null +++ b/src/controllers/inference_controller.py @@ -0,0 +1,139 @@ +# src/controllers/inference_controller.py +import os, queue, cv2, json +from PyQt5.QtWidgets import QMessageBox + +from src.models.inference_worker import InferenceWorkerThread +from src.config import UTILS_DIR, FW_DIR + +class InferenceController: + def __init__(self, main_window, device_controller): + self.main_window = main_window + self.device_controller = device_controller + self.inference_worker = None + self.inference_queue = queue.Queue(maxsize=10) + self.current_tool_config = None + + 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") + + # 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: + # 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") + 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") + + return True + + 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) + + 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 diff --git a/src/controllers/media_controller.py b/src/controllers/media_controller.py new file mode 100644 index 0000000..956233c --- /dev/null +++ b/src/controllers/media_controller.py @@ -0,0 +1,166 @@ +import cv2 +import os +from PyQt5.QtWidgets import QFileDialog +from PyQt5.QtGui import QPixmap +from PyQt5.QtCore import Qt + +from src.models.video_thread import VideoThread +from src.utils.image_utils import qimage_to_numpy + +class MediaController: + def __init__(self, main_window, inference_controller): + self.main_window = main_window + self.inference_controller = inference_controller + self.video_thread = None + self.recording = False + self.recording_audio = False + self.recorded_frames = [] + + 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") + + 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") + + 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) + + except Exception as e: + print(f"Error in update_image: {e}") + + def record_video(self, button=None): + """Start or stop video recording""" + if not self.recording: + try: + self.recording = True + self.recorded_frames = [] + print("Started video recording") + + if button: + button.setStyleSheet(""" + QPushButton { + background: rgba(255, 0, 0, 0.3); + border: 1px solid red; + border-radius: 10px; + } + """) + except Exception as e: + print(f"Error starting video recording: {e}") + else: + try: + self.recording = False + print("Stopped video recording") + + if self.recorded_frames: + filename = QFileDialog.getSaveFileName( + self.main_window, + "Save Video", + "", + "Video Files (*.avi)" + )[0] + + if filename: + height, width = self.recorded_frames[0].shape[:2] + out = cv2.VideoWriter( + filename, + cv2.VideoWriter_fourcc(*'XVID'), + 20.0, + (width, height) + ) + + for frame in self.recorded_frames: + out.write(frame) + out.release() + print(f"Video saved to {filename}") + + if button: + button.setStyleSheet(""" + QPushButton { + background: transparent; + border: 1px transparent; + border-radius: 10px; + } + QPushButton:hover { + background-color: rgba(255, 255, 255, 50); + } + """) + except Exception as e: + print(f"Error stopping video recording: {e}") + + def record_audio(self, button=None): + """Start or stop audio recording""" + if not self.recording_audio: + try: + self.recording_audio = True + print("Started audio recording") + + if button: + button.setStyleSheet(""" + QPushButton { + background: rgba(255, 0, 0, 0.3); + border: 1px solid red; + border-radius: 10px; + } + """) + except Exception as e: + print(f"Error starting audio recording: {e}") + else: + try: + self.recording_audio = False + print("Stopped audio recording") + + if button: + button.setStyleSheet(""" + QPushButton { + background: transparent; + border: 1px transparent; + border-radius: 10px; + } + QPushButton:hover { + background-color: rgba(255, 255, 255, 50); + } + """) + except Exception as e: + print(f"Error stopping audio recording: {e}") + + def take_screenshot(self): + """Take a screenshot of the current frame""" + try: + if self.main_window.canvas_label.pixmap(): + filename = QFileDialog.getSaveFileName( + self.main_window, + "Save Screenshot", + "", + "Image Files (*.png *.jpg)" + )[0] + + if filename: + 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 diff --git a/src/models/inference_worker.py b/src/models/inference_worker.py new file mode 100644 index 0000000..c28cd65 --- /dev/null +++ b/src/models/inference_worker.py @@ -0,0 +1,73 @@ +import os, time, queue, numpy as np, importlib.util +from PyQt5.QtCore import QThread, pyqtSignal +from src.config import UTILS_DIR + +def load_inference_module(mode, model_name): + """Dynamically load an inference module""" + script_path = os.path.join(UTILS_DIR, mode, model_name, "script.py") + module_name = f"{mode}_{model_name}" + spec = importlib.util.spec_from_file_location(module_name, script_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + +class InferenceWorkerThread(QThread): + inference_result_signal = pyqtSignal(object) + + def __init__(self, frame_queue, mode, model_name, min_interval=0.5, mse_threshold=500, once_mode=False): + super().__init__() + self.frame_queue = frame_queue + self.mode = mode + self.model_name = model_name + self.min_interval = min_interval + self.mse_threshold = mse_threshold + self._running = True + self.once_mode = once_mode + self.last_inference_time = 0 + self.last_frame = None + self.cached_result = None + self.input_params = {} + + # Dynamically load inference module + self.inference_module = load_inference_module(mode, model_name) + + def run(self): + while self._running: + try: + frame = self.frame_queue.get(timeout=0.1) + except queue.Empty: + continue + + current_time = time.time() + if current_time - self.last_inference_time < self.min_interval: + 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 + + try: + result = self.inference_module.inference(frame, params=self.input_params) + except Exception as e: + print(f"Inference error: {e}") + result = None + + self.last_inference_time = current_time + self.last_frame = frame.copy() + self.cached_result = result + self.inference_result_signal.emit(result) + + if self.once_mode: + self._running = False + break + + self.quit() + + def stop(self): + self._running = False + self.wait() \ No newline at end of file diff --git a/src/models/video_thread.py b/src/models/video_thread.py new file mode 100644 index 0000000..250952b --- /dev/null +++ b/src/models/video_thread.py @@ -0,0 +1,30 @@ +import cv2 +from PyQt5.QtCore import QThread, pyqtSignal +from PyQt5.QtGui import QImage + +class VideoThread(QThread): + change_pixmap_signal = pyqtSignal(QImage) + + def __init__(self): + super().__init__() + self._run_flag = True + + 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() + + def stop(self): + self._run_flag = False + self.wait() \ No newline at end of file diff --git a/src/services/device_service.py b/src/services/device_service.py index a71dba2..def1533 100644 --- a/src/services/device_service.py +++ b/src/services/device_service.py @@ -1,45 +1,69 @@ -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") -# # 模擬設備描述符 -# device_descriptors = [ -# { -# "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 device_descriptors +# 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() + +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, + # "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 device_descriptors # def get_dongle_type(self, product_id): # for dongle_type in self.K_: diff --git a/src/services/file_service.py b/src/services/file_service.py new file mode 100644 index 0000000..9616347 --- /dev/null +++ b/src/services/file_service.py @@ -0,0 +1,77 @@ +import os +import shutil +from PyQt5.QtWidgets import QFileDialog, QMessageBox + +class FileService: + def __init__(self, main_window, upload_dir): + self.main_window = main_window + self.upload_dir = upload_dir + self.destination = None + + def upload_file(self): + """Handle file upload process""" + try: + print("Calling QFileDialog.getOpenFileName") + options = QFileDialog.Options() + file_path, _ = QFileDialog.getOpenFileName( + self.main_window, + "Upload File", + "", + "All Files (*)", + options=options + ) + print("File path obtained:", file_path) + + if file_path: + print("Checking if upload directory exists") + if not os.path.exists(self.upload_dir): + os.makedirs(self.upload_dir) + print(f"Created UPLOAD_DIR: {self.upload_dir}") + + print("Checking if original file exists:", file_path) + if not os.path.exists(file_path): + self.show_message(QMessageBox.Critical, "Error", "Selected file not found") + return None + + file_name = os.path.basename(file_path) + self.destination = os.path.join(self.upload_dir, file_name) + print("Target path:", self.destination) + + # Check if target path is writable + try: + print("Testing file write permission") + with open(self.destination, 'wb') as test_file: + pass + os.remove(self.destination) + print("Test file creation and deletion successful") + except PermissionError: + self.show_message(QMessageBox.Critical, "Error", "Cannot write to target directory") + 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}") + + 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)}") + return None + + 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) + msgBox.setText(message) + msgBox.setStyleSheet(""" + QLabel { color: white; } + QPushButton { color: white; } + QMessageBox { background-color: #2b2b2b; } + """) + msgBox.exec_() \ No newline at end of file diff --git a/src/services/model_service.py b/src/services/model_service.py index 225d04d..e4280ca 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/utils/config_utils.py b/src/utils/config_utils.py new file mode 100644 index 0000000..8aeac67 --- /dev/null +++ b/src/utils/config_utils.py @@ -0,0 +1,116 @@ +import os +import json +from src.config import UTILS_DIR, SCRIPT_CONFIG + +class ConfigUtils: + @staticmethod + def generate_global_config(): + """Scan directory structure and generate global configuration file""" + try: + config = {"plugins": []} + + # Ensure utils directory exists + if not os.path.exists(UTILS_DIR): + os.makedirs(UTILS_DIR, exist_ok=True) + print(f"Created UTILS_DIR: {UTILS_DIR}") + + # List items in utils directory for debugging + print(f"UTILS_DIR contents: {os.listdir(UTILS_DIR) if os.path.exists(UTILS_DIR) else 'Directory does not exist'}") + + # Scan mode directories (first level subdirectories) + mode_dirs = [d for d in os.listdir(UTILS_DIR) + if os.path.isdir(os.path.join(UTILS_DIR, d)) and not d.startswith('_')] + + print(f"Found mode directories: {mode_dirs}") + + for mode_name in mode_dirs: + mode_path = os.path.join(UTILS_DIR, mode_name) + + mode_info = { + "mode": mode_name, + "display_name": mode_name.replace("_", " ").title(), + "models": [] + } + + # List items in mode directory for debugging + print(f"Contents of mode {mode_name}: {os.listdir(mode_path)}") + + # Scan model directories (second level subdirectories) + model_dirs = [d for d in os.listdir(mode_path) + if os.path.isdir(os.path.join(mode_path, d)) and not d.startswith('_')] + + print(f"Found models in mode {mode_name}: {model_dirs}") + + for model_name in model_dirs: + model_path = os.path.join(mode_path, model_name) + + # Check for model configuration file + 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: + model_config = json.load(f) + + print(f"Successfully read model config: {model_config_path}") + + model_summary = { + "name": model_name, + "display_name": model_config.get("display_name", model_name.replace("_", " ").title()), + "description": model_config.get("description", ""), + "compatible_devices": model_config.get("compatible_devices", []) + } + + mode_info["models"].append(model_summary) + except Exception as e: + print(f"Error reading model config {model_config_path}: {e}") + else: + print(f"Model config file not found: {model_config_path}") + # Optionally create template config file here + + # Only add modes with models + if mode_info["models"]: + config["plugins"].append(mode_info) + + # Write the configuration file + os.makedirs(os.path.dirname(SCRIPT_CONFIG), exist_ok=True) + with open(SCRIPT_CONFIG, "w", encoding="utf-8") as f: + json.dump(config, f, indent=2, ensure_ascii=False) + + print(f"Global configuration generated: {SCRIPT_CONFIG}") + return config + except Exception as e: + print(f"Error generating global configuration: {e}") + import traceback + traceback.print_exc() + return {"plugins": []} + + @staticmethod + def create_model_config_template(model_path): + """Create a template configuration file for a model""" + try: + model_name = os.path.basename(model_path) + mode_name = os.path.basename(os.path.dirname(model_path)) + + template_config = { + "display_name": model_name.replace("_", " ").title(), + "description": f"AI model for {model_name.replace('_', ' ')}", + "model_file": f"{model_name}.nef", + "input_info": { + "type": "video", # Default to video + "supported_formats": ["mp4", "avi"] + }, + "input_parameters": { + "threshold": 0.5 + }, + "compatible_devices": ["KL520", "KL720"] + } + + config_path = os.path.join(model_path, "config.json") + with open(config_path, "w", encoding="utf-8") as f: + json.dump(template_config, f, indent=2, ensure_ascii=False) + + print(f"Created template config for {model_name}") + return True + except Exception as e: + print(f"Error creating model config template: {e}") + return False \ No newline at end of file diff --git a/src/utils/image_utils.py b/src/utils/image_utils.py new file mode 100644 index 0000000..67c7cd7 --- /dev/null +++ b/src/utils/image_utils.py @@ -0,0 +1,12 @@ +import numpy as np +from PyQt5.QtGui import QImage + +def qimage_to_numpy(qimage): + """Convert a QImage to a numpy array""" + qimage = qimage.convertToFormat(QImage.Format_RGB888) + width = qimage.width() + height = qimage.height() + ptr = qimage.bits() + ptr.setsize(qimage.byteCount()) + arr = np.array(ptr).reshape(height, width, 3) + return arr \ No newline at end of file diff --git a/src/views/components/canvas_area.py b/src/views/components/canvas_area.py new file mode 100644 index 0000000..fbdbec9 --- /dev/null +++ b/src/views/components/canvas_area.py @@ -0,0 +1,23 @@ +from PyQt5.QtWidgets import QFrame, QVBoxLayout, QLabel +from PyQt5.QtCore import Qt + +def create_canvas_area(parent): + """Create the canvas area for video display""" + try: + # Create frame container for canvas + canvas_frame = QFrame(parent) + canvas_frame.setStyleSheet("border: 1px solid gray; background: black; border-radius: 20px;") + canvas_frame.setFixedSize(900, 750) + canvas_layout = QVBoxLayout(canvas_frame) + canvas_layout.setContentsMargins(10, 10, 10, 10) + + # Create label for video display + canvas_label = QLabel() + canvas_label.setAlignment(Qt.AlignCenter) + canvas_label.setStyleSheet("border: none; background: transparent;") + canvas_layout.addWidget(canvas_label) + + return canvas_frame, canvas_label + except Exception as e: + print(f"Error in create_canvas_area: {e}") + return QFrame(parent), QLabel(parent) \ No newline at end of file diff --git a/src/views/components/device_list.py b/src/views/components/device_list.py new file mode 100644 index 0000000..27bd654 --- /dev/null +++ b/src/views/components/device_list.py @@ -0,0 +1,60 @@ +from PyQt5.QtWidgets import QFrame, QVBoxLayout, QHBoxLayout, QLabel, QListWidget, QWidget, QPushButton +from PyQt5.QtSvg import QSvgWidget +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QPixmap +import os + +from src.config import SECONDARY_COLOR, UXUI_ASSETS, BUTTON_STYLE, DongleIconMap + +def create_device_layout(parent, device_controller): + """Create the device list layout""" + try: + devices_frame = QFrame(parent) + devices_frame.setStyleSheet(f"border: none; background: {SECONDARY_COLOR}; border-radius: 15px;") + + # Set height based on connected devices + base_height = 250 + extra_height = 100 if len(device_controller.connected_devices) > 1 else 0 + devices_frame.setFixedHeight(base_height + extra_height) + devices_frame.setFixedWidth(240) + + devices_layout = QVBoxLayout(devices_frame) + + # Title + title_layout = QHBoxLayout() + + title_container = QWidget() + container_layout = QHBoxLayout(title_container) + container_layout.setSpacing(10) + + device_icon = QSvgWidget(os.path.join(UXUI_ASSETS, "Assets_svg/ic_window_device.svg")) + device_icon.setFixedSize(20, 20) + container_layout.addWidget(device_icon) + + title_label = QLabel("Device") + title_label.setStyleSheet("color: white; font-size: 20px; font-weight: bold;") + container_layout.addWidget(title_label) + + title_layout.addWidget(title_container) + devices_layout.addLayout(title_layout) + + # Device list + device_list_widget = QListWidget(parent) + devices_layout.addWidget(device_list_widget) + + # Detail button + detail_button = QPushButton("Details", parent) + detail_button.setStyleSheet(BUTTON_STYLE) + detail_button.setFixedSize(72, 30) + detail_button.clicked.connect(parent.show_device_popup) + + button_container = QWidget() + button_layout = QHBoxLayout(button_container) + button_layout.addWidget(detail_button, alignment=Qt.AlignCenter) + button_layout.setContentsMargins(0, 0, 0, 0) + devices_layout.addWidget(button_container) + + return devices_frame, device_list_widget + except Exception as e: + print(f"Error in create_device_layout: {e}") + return QFrame(parent), QListWidget(parent) \ No newline at end of file diff --git a/src/views/components/device_popup.py b/src/views/components/device_popup.py new file mode 100644 index 0000000..c2da98f --- /dev/null +++ b/src/views/components/device_popup.py @@ -0,0 +1,76 @@ +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QListWidget, QListWidgetItem +from PyQt5.QtSvg import QSvgWidget +from PyQt5.QtCore import Qt +import os + +from src.config import SECONDARY_COLOR, BUTTON_STYLE, UXUI_ASSETS + +def create_device_popup(parent, device_controller): + """Create a popup window for device connection management""" + try: + # Device connection popup window + popup = QWidget(parent) + popup_width = int(parent.width() * 0.67) + popup_height = int(parent.height() * 0.67) + popup.setFixedSize(popup_width, popup_height) + popup.setStyleSheet(f""" + QWidget {{ + background-color: {SECONDARY_COLOR}; + border-radius: 20px; + padding: 20px; + }} + """) + + popup_layout = QVBoxLayout(popup) + popup_layout.setContentsMargins(0, 0, 0, 0) + + # Title row + title_layout = QHBoxLayout() + title_layout.setAlignment(Qt.AlignCenter) + + title_container = QWidget() + container_layout = QHBoxLayout(title_container) + container_layout.setSpacing(10) + + device_icon = QSvgWidget(os.path.join(UXUI_ASSETS, "Assets_svg/ic_window_device.svg")) + device_icon.setFixedSize(35, 35) + container_layout.addWidget(device_icon) + + popup_label = QLabel("Device Connection") + popup_label.setStyleSheet("color: white; font-size: 32px;") + container_layout.addWidget(popup_label) + + container_layout.setAlignment(Qt.AlignCenter) + title_layout.addWidget(title_container) + popup_layout.addLayout(title_layout) + + # Device list + device_list_widget_popup = QListWidget(popup) + popup_layout.addWidget(device_list_widget_popup) + + # Store reference to this list widget for later use + parent.device_list_widget_popup = device_list_widget_popup + + # Button area + button_layout = QHBoxLayout() + + refresh_button = QPushButton("Refresh") + refresh_button.clicked.connect(device_controller.refresh_devices) + refresh_button.setFixedSize(110, 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.clicked.connect(parent.hide_device_popup) + button_layout.addWidget(done_button) + + button_layout.setSpacing(10) + popup_layout.addSpacing(20) + 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 diff --git a/src/views/components/media_panel.py b/src/views/components/media_panel.py new file mode 100644 index 0000000..bd0b90c --- /dev/null +++ b/src/views/components/media_panel.py @@ -0,0 +1,64 @@ +from PyQt5.QtWidgets import QFrame, QVBoxLayout, QHBoxLayout, QPushButton +from PyQt5.QtSvg import QSvgWidget +from PyQt5.QtCore import Qt +import os + +from src.config import SECONDARY_COLOR, UXUI_ASSETS + +def create_media_panel(parent, media_controller, file_service): + """Create the media control panel with buttons for media operations""" + try: + # Create a vertical layout for the buttons + media_panel = QFrame(parent) + media_panel.setStyleSheet(f"background: {SECONDARY_COLOR}; border-radius: 20px;") + media_layout = QVBoxLayout(media_panel) + media_layout.setAlignment(Qt.AlignCenter) + + # Media button information + media_buttons_info = [ + ('screenshot', os.path.join(UXUI_ASSETS, "Assets_svg/bt_function_screencapture_normal.svg"), + media_controller.take_screenshot), + ('upload file', os.path.join(UXUI_ASSETS, "Assets_svg/bt_function_upload_normal.svg"), + file_service.upload_file), + ('voice', os.path.join(UXUI_ASSETS, "Assets_svg/ic_recording_voice.svg"), + lambda: media_controller.record_audio(None)), + ('video', os.path.join(UXUI_ASSETS, "Assets_svg/ic_recording_camera.svg"), + lambda: media_controller.record_video(None)), + ] + + for button_name, icon_path, callback in media_buttons_info: + button = QPushButton() + button.setFixedSize(50, 50) + button.setStyleSheet(""" + QPushButton { + background: transparent; + color: white; + border: 1px transparent; + border-radius: 10px; + } + QPushButton:hover { + background-color: rgba(255, 255, 255, 50); + } + QPushButton:pressed { + background-color: rgba(255, 255, 255, 100); + } + """) + + button_layout = QHBoxLayout(button) + button_layout.setContentsMargins(0, 0, 0, 0) + button_layout.setAlignment(Qt.AlignCenter) + + icon = QSvgWidget(icon_path) + icon.setFixedSize(40, 40) + button_layout.addWidget(icon) + + button.clicked.connect(callback) + media_layout.addWidget(button, alignment=Qt.AlignCenter) + + media_panel.setLayout(media_layout) + media_panel.setFixedSize(90, 240) + + return media_panel + except Exception as e: + print(f"Error in create_media_panel: {e}") + return QFrame(parent) \ No newline at end of file diff --git a/src/views/components/toolbox.py b/src/views/components/toolbox.py new file mode 100644 index 0000000..1c91a23 --- /dev/null +++ b/src/views/components/toolbox.py @@ -0,0 +1,82 @@ +import json +import os +from PyQt5.QtWidgets import QFrame, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QWidget +from PyQt5.QtSvg import QSvgWidget +from PyQt5.QtCore import Qt + +from src.config import SECONDARY_COLOR, UXUI_ASSETS, BUTTON_STYLE, SCRIPT_CONFIG + +def create_ai_toolbox(parent, config_utils, inference_controller): + """Create the AI toolbox layout""" + try: + # Read JSON configuration + print("config_path:", SCRIPT_CONFIG) + if os.path.exists(SCRIPT_CONFIG): + with open(SCRIPT_CONFIG, "r", encoding="utf-8") as f: + config = json.load(f) + plugins = config.get("plugins", []) + else: + # If no configuration file, try to generate it + plugins = config_utils.generate_global_config().get("plugins", []) + if not plugins: + print("Unable to generate configuration, using empty tool list") + + # Create toolbox UI + toolbox_frame = QFrame(parent) + toolbox_frame.setStyleSheet(f"border: none; background: {SECONDARY_COLOR}; border-radius: 15px;") + toolbox_frame.setFixedHeight(450) + toolbox_frame.setFixedWidth(240) + toolbox_layout = QVBoxLayout(toolbox_frame) + + # Title row + title_layout = QHBoxLayout() + title_container = QWidget() + container_layout = QHBoxLayout(title_container) + container_layout.setSpacing(10) + + toolbox_icon = QSvgWidget(os.path.join(UXUI_ASSETS, "Assets_svg/ic_window_toolbox.svg")) + toolbox_icon.setFixedSize(40, 40) + container_layout.addWidget(toolbox_icon) + + title_label = QLabel("AI Toolbox") + title_label.setStyleSheet("color: white; font-size: 20px; font-weight: bold;") + container_layout.addWidget(title_label) + + title_layout.addWidget(title_container) + toolbox_layout.addLayout(title_layout) + + # Create tool buttons (categorized) + for plugin in plugins: + mode = plugin.get("mode", "") + display_name = plugin.get("display_name", "") + + # Add category title + category_label = QLabel(display_name) + category_label.setStyleSheet("color: white; font-size: 16px; font-weight: bold; margin-top: 10px;") + toolbox_layout.addWidget(category_label) + + # Add all model buttons in this category + for model in plugin.get("models", []): + model_name = model.get("name", "") + display_name = model.get("display_name", "") + + # Create tool configuration + tool_config = { + "mode": mode, + "model_name": model_name, + "display_name": display_name, + "description": model.get("description", ""), + "compatible_devices": model.get("compatible_devices", []) + } + + # Create button + button = QPushButton(display_name) + button.clicked.connect(lambda checked, t=tool_config: inference_controller.select_tool(t)) + button.setStyleSheet(BUTTON_STYLE) + button.setFixedHeight(40) + toolbox_layout.addWidget(button) + + return toolbox_frame + except Exception as e: + print(f"Error in create_ai_toolbox: {e}") + return QFrame(parent) \ No newline at end of file diff --git a/src/views/login_screen.py b/src/views/login_screen.py new file mode 100644 index 0000000..0a7383d --- /dev/null +++ b/src/views/login_screen.py @@ -0,0 +1,157 @@ +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, + QLineEdit, QComboBox, QFrame, QMessageBox) +from PyQt5.QtCore import Qt, pyqtSignal +from PyQt5.QtGui import QPixmap, QFont +import os +from src.config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR + +class LoginScreen(QWidget): + # Signals for navigation + login_success = pyqtSignal() + back_to_selection = pyqtSignal() + + def __init__(self, parent=None): + super().__init__(parent) + self.init_ui() + + def init_ui(self): + # Basic window setup + self.setGeometry(100, 100, *WINDOW_SIZE) + self.setStyleSheet(f"background-color: {BACKGROUND_COLOR};") + + # Main layout + layout = QVBoxLayout(self) + + # 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) + 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) + + # Login form container + form_container = QFrame(self) + form_container.setFrameShape(QFrame.StyledPanel) + form_container.setStyleSheet(""" + QFrame { + background-color: white; + border-radius: 10px; + padding: 20px; + } + """) + form_layout = QVBoxLayout(form_container) + + # Server type + server_label = QLabel("Server Authentication Type", self) + server_label.setFont(QFont("Arial", 12)) + 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) + form_layout.addWidget(self.server_combo) + + form_layout.addSpacing(10) + + # Username + username_label = QLabel("Username", self) + username_label.setFont(QFont("Arial", 12)) + 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)) + form_layout.addWidget(self.username_input) + + form_layout.addSpacing(10) + + # Password + password_label = QLabel("Password", self) + password_label.setFont(QFont("Arial", 12)) + 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)) + 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.hide() + form_layout.addWidget(self.error_label) + + # Buttons + button_layout = QHBoxLayout() + + back_button = QPushButton("Back", self) + back_button.setMinimumHeight(40) + back_button.setFont(QFont("Arial", 12)) + back_button.setStyleSheet(""" + QPushButton { + background-color: #757575; + color: white; + border-radius: 5px; + padding: 5px 15px; + } + QPushButton:hover { + background-color: #616161; + } + """) + 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.setStyleSheet(""" + QPushButton { + background-color: #1E88E5; + color: white; + border-radius: 5px; + padding: 5px 15px; + } + QPushButton:hover { + background-color: #1976D2; + } + """) + login_button.clicked.connect(self.attempt_login) + + button_layout.addWidget(back_button) + button_layout.addWidget(login_button) + + form_layout.addLayout(button_layout) + + # Add form to main layout + layout.addWidget(form_container, 1) + + def attempt_login(self): + username = self.username_input.text() + password = self.password_input.text() + + # For demo purposes, use a simple validation + if not username or not password: + self.show_error("Please enter both username and password") + 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): + self.error_label.setText(message) + self.error_label.show() diff --git a/src/views/mainWindows.py b/src/views/mainWindows.py index dc01712..3db85c0 100644 --- a/src/views/mainWindows.py +++ b/src/views/mainWindows.py @@ -1,198 +1,71 @@ -''' Importing necessary libraries ''' -import kp, cv2, os, sys, json, time, threading, queue, numpy as np, importlib.util, shlex, subprocess, shutil -from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QLabel, QPushButton, - QComboBox, QFileDialog, QMessageBox, QHBoxLayout, QDialog, QListWidget, - QScrollArea, QFrame, QListWidgetItem, QTextEdit, QGridLayout) -from PyQt5.QtSvg import QSvgWidget -from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal -from PyQt5.QtGui import QPixmap, QMovie, QImage +import os, sys, json, queue, numpy as np +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QGridLayout, QFrame, QMessageBox +from PyQt5.QtCore import Qt, QTimer +from PyQt5.QtGui import QPixmap, QMovie -from ..config import (UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR, SECONDARY_COLOR, - BUTTON_STYLE, MASK_STYLE, PROJECT_ROOT, SCRIPT_CONFIG, UTILS_DIR , UPLOAD_DIR, - FW_DIR, DongleModelMap, DongleIconMap) - -from ..services.device_service import check_available_device - -#──────────────────────────────────────────────────────────── -# VideoThread:持續從攝影機擷取影像 -class VideoThread(QThread): - change_pixmap_signal = pyqtSignal(QImage) - - def __init__(self): - super().__init__() - self._run_flag = True - - 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: - # 轉成 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) - # 可依需求延遲控制幀率 - cap.release() - - def stop(self): - self._run_flag = False - self.wait() - -#──────────────────────────────────────────────────────────── -# 輔助 function:將 QImage 轉成 numpy 陣列 -def qimage_to_numpy(qimage): - qimage = qimage.convertToFormat(QImage.Format_RGB888) - width = qimage.width() - height = qimage.height() - ptr = qimage.bits() - ptr.setsize(qimage.byteCount()) - arr = np.array(ptr).reshape(height, width, 3) - return arr - -#──────────────────────────────────────────────────────────── -# 動態載入 inference 模組的函式 -def load_inference_module(mode, model_name): - script_path = os.path.join(UTILS_DIR, mode, model_name, "script.py") - module_name = f"{mode}_{model_name}" - spec = importlib.util.spec_from_file_location(module_name, script_path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - -#──────────────────────────────────────────────────────────── -# InferenceWorkerThread:從 frame_queue 中取出 frame,依照設定頻率處理 frame, -# 利用動態載入的 inference 模組進行推論,並對結果進行緩存。 -class InferenceWorkerThread(QThread): - # 傳出 inference 結果,型態可依需求調整(例如 dict 或 tuple) - inference_result_signal = pyqtSignal(object) - - def __init__(self, frame_queue, mode, model_name, min_interval=0.5, mse_threshold=500, once_mode=False): - """ - frame_queue: 傳入的 frame 佇列(numpy 陣列) - mode: 模式名稱 (如 'face_recognition') - model_name: 模型名稱 (如 'face_detection') - min_interval: 最小 inference 間隔 (秒) - mse_threshold: 當前後 frame 之均方誤差低於此值則視為相似 - """ - super().__init__() - self.frame_queue = frame_queue - self.mode = mode - self.model_name = model_name - self.min_interval = min_interval - self.mse_threshold = mse_threshold - self._running = True - self.once_mode = once_mode - self.last_inference_time = 0 - self.last_frame = None - self.cached_result = None - - # 動態載入 inference 模組 - script_path = os.path.join(UTILS_DIR, mode, model_name, "script.py") - self.inference_module = load_inference_module(mode, model_name) - - def run(self): - while self._running: - try: - # 若佇列空,等待 0.1 秒 - frame = self.frame_queue.get(timeout=0.1) - except queue.Empty: - continue - - current_time = time.time() - # 檢查頻率:如果離上次 inference 還不到 min_interval,則跳過處理 - if current_time - self.last_inference_time < self.min_interval: - continue - - # 如果有緩存結果且 frame 與上次非常相似則直接使用 - 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 - - # 呼叫動態載入的 inference 模組處理 frame - try: - # 使用從主程式傳遞過來的 input_params - result = self.inference_module.inference(frame, params=self.input_params) - except Exception as e: - print(f"Inference error: {e}") - result = None - - self.last_inference_time = current_time - self.last_frame = frame.copy() - self.cached_result = result - self.inference_result_signal.emit(result) - - if self.once_mode: - # 設定停止旗標,然後退出迴圈 - self._running = False - break - - # 當迴圈結束時呼叫 quit() 以讓 thread 結束 - self.quit() - - def stop(self): - self._running = False - self.wait() +from src.config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR, UPLOAD_DIR +from src.controllers.device_controller import DeviceController +from src.controllers.inference_controller import InferenceController +from src.controllers.media_controller import MediaController +from src.services.file_service import FileService +from src.utils.config_utils import ConfigUtils +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 class MainWindow(QWidget): def __init__(self): super().__init__() - self.connected_devices = [] - self.video_thread = None - self.recording = False - self.recording_audio = False - self.video_writer = None - self.recorded_frames = [] + + # Initialize controllers + self.device_controller = DeviceController(self) + self.inference_controller = InferenceController(self, self.device_controller) + self.media_controller = MediaController(self, self.inference_controller) + + # Initialize services + self.file_service = FileService(self, UPLOAD_DIR) + self.config_utils = ConfigUtils() + + # Set up UI and configuration self.destination = None - self.current_tool_config = None - self.inference_worker = None - self.inference_queue = queue.Queue(maxsize=10) - - # 確保目錄存在並更新配置 self.generate_global_config() - self.init_ui() - def init_ui(self): # 初始化UI (暫時不需要修改) + def init_ui(self): try: - # 基本視窗設定 + # Basic window setup self.setGeometry(100, 100, *WINDOW_SIZE) - self.setWindowTitle('Innovedus AI Playground') + self.setWindowTitle('Kneron Academy AI Playground') self.setStyleSheet(f"background-color: {BACKGROUND_COLOR};") - # 主要佈局 + + # Main layout self.layout = QVBoxLayout(self) self.layout.setContentsMargins(0, 0, 0, 0) self.setLayout(self.layout) - # 載入歡迎畫面 + + # Show welcome screen self.show_welcome_label() - # 0.5秒後切換到主頁面和設備連接彈窗 + + # Switch to main page and device popup after delay QTimer.singleShot(500, self.show_device_popup_and_main_page) except Exception as e: print(f"Error in init_ui: {e}") - - def show_welcome_label(self): # 歡迎頁面(暫時不需要修改) + + def show_welcome_label(self): try: welcome_label = QLabel(self) logo_path = os.path.join(UXUI_ASSETS, "Assets_png/kneron_logo.png") print(f"Loading logo from: {logo_path}") if not os.path.exists(logo_path): - print(f"錯誤:找不到圖片檔案:{logo_path}") + print(f"Error: Logo file not found: {logo_path}") return welcome_pixmap = QPixmap(logo_path) if welcome_pixmap.isNull(): - print(f"錯誤:無法載入圖片:{logo_path}") + print(f"Error: Unable to load image: {logo_path}") return welcome_label.setPixmap(welcome_pixmap) @@ -200,73 +73,39 @@ class MainWindow(QWidget): self.layout.addWidget(welcome_label) except Exception as e: print(f"Error in show_welcome_label: {e}") - - def device_popup_mask_setup(self): # TODO: 需要修改 popup windows 的佈局 - try: - print("setting up popup mask") - # 添加半透明遮罩 - self.overlay = QWidget(self) - # 確保遮罩層在最上層 - self.overlay.raise_() - - self.overlay.setStyleSheet(MASK_STYLE) - self.overlay.setGeometry(0, 0, self.width(), self.height()) - self.overlay_layout = QVBoxLayout(self.overlay) - - # 設備連接彈窗 - self.device_popup = self.create_device_popup() - self.overlay_layout.addWidget(self.device_popup, alignment=Qt.AlignCenter) - - print("finish popup windows setup") - except Exception as e: - print(f"Error in device_popup_mask_setup: {e}") - def show_device_popup_and_main_page(self): try: - # 清除歡迎頁面 + # Clear welcome screen self.clear_layout() - # 1. 先初始化主頁面 + # 1. Initialize main page self.main_page = self.create_main_page() self.layout.addWidget(self.main_page) - print("finish setup main page") + print("Main page setup complete") - # 2. 再設定 popup 和遮罩 + # 2. Set up popup and mask self.device_popup_mask_setup() - # 3. 刷新設備 - self.refresh_devices() + # 3. Refresh devices + self.device_controller.refresh_devices() - # 4. 顯示 popup + # 4. Show popup self.show_device_popup() - - # 5. 啟動相機 -> 這邊先註解掉, 上傳資料和使用相機會打架 -> 移到 select tool 的部分決定是否啟動相機 (20250206) - # self.start_camera() except Exception as e: print(f"Error in show_device_popup_and_main_page: {e}") - - def clear_layout(self): - try: - # 清除所有小工具 - while self.layout.count(): - child = self.layout.takeAt(0) - if child.widget(): - child.widget().deleteLater() - except Exception as e: - print(f"Error in clear_layout: {e}") - + def create_main_page(self): try: main_page = QWidget(self) main_layout = QHBoxLayout(main_page) main_page.setLayout(main_layout) - # 左側佈局 + # Left layout left_layout = QVBoxLayout() main_layout.addLayout(left_layout, 1) - # 添加 Kneron logo + # Add Kneron logo logo_label = QLabel() logo_path = os.path.join(UXUI_ASSETS, "Assets_png/kneron_logo.png") logo_pixmap = QPixmap(logo_path) @@ -274,921 +113,58 @@ class MainWindow(QWidget): logo_label.setPixmap(scaled_logo) left_layout.addWidget(logo_label) - # 添加其他元件 - self.create_device_layout(left_layout) - self.create_ai_toolbox(left_layout) + # Add device list and AI toolbox + self.device_frame, self.device_list_widget = create_device_layout(self, self.device_controller) + left_layout.addWidget(self.device_frame) + + self.toolbox_frame = create_ai_toolbox(self, self.config_utils, self.inference_controller) + left_layout.addWidget(self.toolbox_frame) - # 右側佈局 + # Right layout right_container = QWidget() right_layout = QGridLayout(right_container) right_layout.setContentsMargins(0, 0, 0, 0) right_layout.setSpacing(0) main_layout.addWidget(right_container, 2) - # 添加 Canvas Area - self.canvas_area = self.create_canvas_area() - right_layout.addWidget(self.canvas_area, 0, 0, 1, 1) # 添加到 (0,0) 格子,跨越1行1列 + # Add Canvas Area + self.canvas_frame, self.canvas_label = create_canvas_area(self) + right_layout.addWidget(self.canvas_frame, 0, 0, 1, 1) - # 添加 Media Panel,並對齊到右下角 - self.media_panel = self.create_media_panel() - right_layout.addWidget(self.media_panel, 0, 0, Qt.AlignBottom | Qt.AlignRight) # 疊加在 (0,0) 格子,右下對齊 + # Add Media Panel aligned to bottom right + self.media_panel = create_media_panel(self, self.media_controller, self.file_service) + right_layout.addWidget(self.media_panel, 0, 0, Qt.AlignBottom | Qt.AlignRight) main_layout.addWidget(right_container, 2) return main_page except Exception as e: print(f"Error in create_main_page: {e}") - - # 主畫面右上角的 setting button, 還沒連接任何視窗畫面 - def create_settings_button(self): - """ - 建立並回傳一個 'Settings' 按鈕 (內含 SVG) 的物件, - 並設定好大小、樣式等。 - """ - button = QPushButton(self) - button.setFixedSize(106, 24) # 您說明的尺寸 - - # 建立一個 QSvgWidget 放在按鈕上 - svg_widget = QSvgWidget(os.path.join(UXUI_ASSETS, "Assets_svg", "btn_setting.svg")) - svg_widget.setFixedSize(106, 24) - - # 建立水平布局,使 SVG 可以貼齊按鈕 - layout = QHBoxLayout(button) - layout.setContentsMargins(0, 0, 0, 0) # 去除預設邊距 - layout.addWidget(svg_widget) - - # 若要讓按鈕有點透明或 hover/press 效果,可再加上 styleSheet - button.setStyleSheet(""" - QPushButton { - border: none; - background: transparent; - } - QPushButton:hover { - background-color: rgba(255, 255, 255, 50); - } - QPushButton:pressed { - background-color: rgba(255, 255, 255, 100); - } - """) - - # 可依需求綁定點擊事件 - # button.clicked.connect(self.on_settings_button_clicked) - - return button - - def create_device_layout(self, layout): - try: - devices_frame = QFrame(self) - devices_frame.setStyleSheet(f"border: none; background: {SECONDARY_COLOR}; border-radius: 15px;") - - # 根據連接設備數量設置高度 - base_height = 250 # 基本高度 - print(self.connected_devices) - extra_height = 100 if len(self.connected_devices) > 1 else 0 # 如果設備超過2個,增加100 - devices_frame.setFixedHeight(base_height + extra_height) - devices_frame.setFixedWidth(240) - - devices_layout = QVBoxLayout(devices_frame) - - # 標題 - title_layout = QHBoxLayout() - - # 容器來放置圖示和標籤 - title_container = QWidget() - container_layout = QHBoxLayout(title_container) - container_layout.setSpacing(10) - - device_icon = QSvgWidget(os.path.join(UXUI_ASSETS, "Assets_svg/ic_window_device.svg")) - device_icon.setFixedSize(20, 20) - container_layout.addWidget(device_icon) - - title_label = QLabel("Device") - title_label.setStyleSheet("color: white; font-size: 20px; font-weight: bold;") - container_layout.addWidget(title_label) - - # 將容器添加到標題布局中 - title_layout.addWidget(title_container) - devices_layout.addLayout(title_layout) - - # 設備列表 - self.device_list_widget = QListWidget(self) - devices_layout.addWidget(self.device_list_widget) - - # 詳細按鈕 - detail_button = QPushButton("Details", self) - detail_button.setStyleSheet(BUTTON_STYLE) - detail_button.setFixedSize(72, 30) # 固定按鈕大小 - detail_button.clicked.connect(self.show_device_details) - - # 創建一個容器用於按鈕置中 - button_container = QWidget() - button_layout = QHBoxLayout(button_container) - button_layout.addWidget(detail_button, alignment=Qt.AlignCenter) # 按鈕置中 - button_layout.setContentsMargins(0, 0, 0, 0) # 去掉邊距 - devices_layout.addWidget(button_container) # 添加容器到主布局 - - layout.addWidget(devices_frame) - except Exception as e: - print(f"Error in create_device_layout: {e}") - - def update_device_frame_size(self): - """更新設備框架的大小""" - if hasattr(self, 'device_list_widget'): - frame = self.device_list_widget.parent() - while not isinstance(frame, QFrame): - frame = frame.parent() - - base_height = 300 - extra_height = 100 if len(self.connected_devices) > 2 else 0 - frame.setFixedHeight(base_height + extra_height) - - def select_device(self, device, list_item): - """ - 當使用者點選某個 dongle 項目時,記錄該設備資料,並更新 UI 樣式 - """ - self.selected_device = device - print("選取的 dongle:", device) - - # 更新列表中所有項目的背景顏色(例如:清除其他項目的選取狀態) - for index in range(self.device_list_widget.count()): - item = self.device_list_widget.item(index) - widget = self.device_list_widget.itemWidget(item) - # 設定預設背景 - widget.setStyleSheet("background: none;") - - # 將被選取的項目背景改變(例如:淺藍色) - list_item_widget = self.device_list_widget.itemWidget(list_item) - list_item_widget.setStyleSheet("background-color: lightblue;") - - def add_device_to_list(self, device): - try: - usb_port_id = device.get("usb_port_id") - product_id = device.get("product_id") - converted_product_id = hex(product_id).strip().lower() - kn_number = device.get("kn_number") - - icon_filename = DongleIconMap.get(converted_product_id) - icon_path = os.path.join(UXUI_ASSETS, "Assets_png", icon_filename) - - item_widget = QWidget() - item_layout = QHBoxLayout(item_widget) - item_layout.setContentsMargins(5, 5, 5, 5) - - # 疊放圖示和框框的容器 - icon_container = QWidget() - icon_container.setFixedSize(44, 44) - - # 藍色背景框 - box_label = QLabel(icon_container) - box_label.setFixedSize(35, 35) - box_label.setStyleSheet(""" - background-color: #182D4B; - border-radius: 5px; - """) - box_label.move(4, 4) - - # 圖示 - icon_label = QLabel(icon_container) - icon_label.setFixedSize(29, 25) - icon_label.setStyleSheet("background: transparent;") - pixmap = QPixmap(icon_path) - scaled_pixmap = pixmap.scaled(29, 25, Qt.KeepAspectRatio, Qt.SmoothTransformation) - icon_label.setPixmap(scaled_pixmap) - icon_label.setAttribute(Qt.WA_TranslucentBackground) - icon_label.move(8, 9) - - item_layout.addWidget(icon_container) - - # 文字資訊 - text_layout = QVBoxLayout() - text_layout.setSpacing(0) - label_text = QLabel("序號", self) - label_text.setStyleSheet("color: white; font-size: 12px;") - text_layout.addWidget(label_text) - value_text = QLabel(f"{kn_number}", self) - value_text.setStyleSheet("color: white; font-size: 12px;") - text_layout.addWidget(value_text) - item_layout.addLayout(text_layout) - - # 建立 QListWidgetItem 並關聯 widget - list_item = QListWidgetItem() - list_item.setSizeHint(item_widget.sizeHint()) - self.device_list_widget.addItem(list_item) - self.device_list_widget.setItemWidget(list_item, item_widget) - - # 綁定點擊事件:當使用者點擊該 widget 時,更新 selected_device - item_widget.mousePressEvent = lambda event, dev=device, item=list_item: self.select_device(dev, item) - except Exception as e: - print(f"Error in add_device_to_list: {e}") - - # 當使用者選擇工具時更新 inference 模組 - def select_tool(self, tool_config): - print("選擇工具:", tool_config.get("display_name")) - self.current_tool_config = tool_config - - # 獲取模式和模型名稱 - 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") - detailed_config = {} - - 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}") - - # 獲取工具的輸入類型 - input_info = tool_config.get("input_info", {}) - tool_type = input_info.get("type", "video") - print("type:", tool_type) - once_mode = True if tool_type == "image" else False - - # 組合input_params - input_params = tool_config.get("input_parameters", {}).copy() - - # 處理設備相關設定 - if hasattr(self, "selected_device") and self.selected_device: - input_params["usb_port_id"] = self.selected_device.get("usb_port_id", 0) - dongle = self.selected_device.get("dongle", "unknown") - print("選取的 dongle:", dongle) - - # 檢查模型是否支援該設備 - compatible_devices = tool_config.get("compatible_devices", []) - if compatible_devices and dongle not in compatible_devices: - self.show_custom_message( - QMessageBox.Warning, - "設備不兼容", - f"所選模型不支援 {dongle} 設備。\n支援的設備: {', '.join(compatible_devices)}" - ) - return - - # 處理韌體路徑 - 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: - # 預設設備處理邏輯不變 - if self.connected_devices and len(self.connected_devices) > 0: - input_params["usb_port_id"] = self.connected_devices[0].get("usb_port_id", 0) - print("Warning: 沒有特別選取 dongle, 預設使用第一個設備") - else: - input_params["usb_port_id"] = 0 - print("Warning: 沒有連接設備, 使用預設 usb_port_id 0") - - # 處理檔案輸入 - if tool_type in ["image", "voice"]: - # 處理邏輯不變 - if hasattr(self, "destination") and self.destination: - input_params["file_path"] = self.destination - uploaded_img = cv2.imread(self.destination) - if uploaded_img is not None: - if not self.inference_queue.full(): - self.inference_queue.put(uploaded_img) - print("上傳的圖片已推入 inference queue") - else: - print("Warning: inference queue 已滿,無法推入上傳圖片") - else: - print("Warning: 無法讀取上傳的圖片") - else: - input_params["file_path"] = "" - print("Warning: 需要檔案輸入,但尚未上傳檔案。") - - # 添加模型檔案路徑 - 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_params:", input_params) - - # 如果已有 worker 運行,先停止舊 worker - if self.inference_worker: - self.inference_worker.stop() - self.inference_worker = None - - # 建立新的 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.handle_inference_result) - self.inference_worker.start() - print(f"Inference worker 已切換到模組:{mode}/{model_name}") - - if tool_type == "video": - self.start_camera() - else: - print("工具模式為非 video,不啟動相機") - - # 修改 create_ai_toolbox 方法:每個按鈕點擊時呼叫 select_tool - def create_ai_toolbox(self, layout): - try: - # 讀取 JSON 配置 - print("config_path: ", SCRIPT_CONFIG) - if os.path.exists(SCRIPT_CONFIG): - with open(SCRIPT_CONFIG, "r", encoding="utf-8") as f: - config = json.load(f) - plugins = config.get("plugins", []) - else: - # 若無配置檔,則嘗試自動生成 - plugins = self.generate_global_config().get("plugins", []) - if not plugins: - print("無法生成配置,使用空的工具列表") - - # 創建工具箱UI - toolbox_frame = QFrame(self) - toolbox_frame.setStyleSheet(f"border: none; background: {SECONDARY_COLOR}; border-radius: 15px;") - toolbox_frame.setFixedHeight(450) - toolbox_frame.setFixedWidth(240) - toolbox_layout = QVBoxLayout(toolbox_frame) - - # 標題列 - title_layout = QHBoxLayout() - title_container = QWidget() - container_layout = QHBoxLayout(title_container) - container_layout.setSpacing(10) - - toolbox_icon = QSvgWidget(os.path.join(UXUI_ASSETS, "Assets_svg/ic_window_toolbox.svg")) - toolbox_icon.setFixedSize(40, 40) - container_layout.addWidget(toolbox_icon) - - title_label = QLabel("AI Toolbox") - title_label.setStyleSheet("color: white; font-size: 20px; font-weight: bold;") - container_layout.addWidget(title_label) - - title_layout.addWidget(title_container) - toolbox_layout.addLayout(title_layout) - - # 建立工具按鈕 (分類顯示) - for plugin in plugins: - mode = plugin.get("mode", "") - display_name = plugin.get("display_name", "") - - # 添加分類標題 - category_label = QLabel(display_name) - category_label.setStyleSheet("color: white; font-size: 16px; font-weight: bold; margin-top: 10px;") - toolbox_layout.addWidget(category_label) - - # 添加該分類下的所有模型按鈕 - for model in plugin.get("models", []): - model_name = model.get("name", "") - display_name = model.get("display_name", "") - - # 建立工具配置 - tool_config = { - "mode": mode, - "model_name": model_name, - "display_name": display_name, - "description": model.get("description", ""), - "compatible_devices": model.get("compatible_devices", []) - } - - # 建立按鈕 - button = QPushButton(display_name) - button.clicked.connect(lambda checked, t=tool_config: self.select_tool(t)) - button.setStyleSheet(BUTTON_STYLE) - button.setFixedHeight(40) - toolbox_layout.addWidget(button) - - layout.addWidget(toolbox_frame) - except Exception as e: - print(f"Error in create_ai_toolbox: {e}") - - def create_canvas_area(self): - try: - # Create frame container for canvas - canvas_frame = QFrame(self) - canvas_frame.setStyleSheet("border: 1px solid gray; background: black; border-radius: 20px;") - canvas_frame.setFixedSize(900, 750) - canvas_layout = QVBoxLayout(canvas_frame) - canvas_layout.setContentsMargins(10, 10, 10, 10) - - # Create label for video display - self.canvas_label = QLabel() - self.canvas_label.setAlignment(Qt.AlignCenter) - self.canvas_label.setStyleSheet("border: none; background: transparent;") - canvas_layout.addWidget(self.canvas_label) - - return canvas_frame - except Exception as e: - print(f"Error in create_canvas_area: {e}") - return QFrame(self) - - def create_media_panel(self): - try: - # 創建一個垂直佈局來放置按鈕 - media_panel = QFrame(self) - media_panel.setStyleSheet(f"background: {SECONDARY_COLOR}; border-radius: 20px;") - media_layout = QVBoxLayout(media_panel) - media_layout.setAlignment(Qt.AlignCenter) - - # 媒體按鈕資訊 - media_buttons_info = [ - ('screenshot', os.path.join(UXUI_ASSETS, "Assets_svg/bt_function_screencapture_normal.svg")), - ('upload file', os.path.join(UXUI_ASSETS, "Assets_svg/bt_function_upload_normal.svg")), - ('voice', os.path.join(UXUI_ASSETS, "Assets_svg/ic_recording_voice.svg")), - ('video', os.path.join(UXUI_ASSETS, "Assets_svg/ic_recording_camera.svg")), - ] - - for button_name, icon_path in media_buttons_info: - button = QPushButton() - button.setFixedSize(50, 50) - button.setStyleSheet(""" - QPushButton { - background: transparent; - color: white; - border: 1px transparent; - border-radius: 10px; - } - QPushButton:hover { - background-color: rgba(255, 255, 255, 50); - } - QPushButton:pressed { - background-color: rgba(255, 255, 255, 100); - } - """) - - button_layout = QHBoxLayout(button) - button_layout.setContentsMargins(0, 0, 0, 0) - button_layout.setAlignment(Qt.AlignCenter) - icon = QSvgWidget(icon_path) - icon.setFixedSize(40, 40) - button_layout.addWidget(icon) - - if button_name == 'video': - button.clicked.connect(self.record_video) - elif button_name == 'voice': - button.clicked.connect(self.record_audio) - elif button_name == 'screenshot': - button.clicked.connect(self.take_screenshot) - elif button_name == 'upload file': - button.clicked.connect(self.upload_file) - - media_layout.addWidget(button, alignment=Qt.AlignCenter) - - media_panel.setLayout(media_layout) - media_panel.setFixedSize(90, 240) - - return media_panel - except Exception as e: - print(f"Error in create_media_panel: {e}") - return QFrame(self) - - def create_device_popup(self): - try: - # 設備連接彈出視窗 - popup = QWidget(self) - popup_width = int(self.width() * 0.67) - popup_height = int(self.height() * 0.67) - popup.setFixedSize(popup_width, popup_height) - popup.setStyleSheet(f""" - QWidget {{ - background-color: {SECONDARY_COLOR}; - border-radius: 20px; - padding: 20px; - }} - """) - - popup_layout = QVBoxLayout(popup) - popup_layout.setContentsMargins(0, 0, 0, 0) - - # 標題列 - title_layout = QHBoxLayout() - title_layout.setAlignment(Qt.AlignCenter) - - title_container = QWidget() - container_layout = QHBoxLayout(title_container) - container_layout.setSpacing(10) - - device_icon = QSvgWidget(os.path.join(UXUI_ASSETS, "Assets_svg/ic_window_device.svg")) - device_icon.setFixedSize(35, 35) - container_layout.addWidget(device_icon) - - popup_label = QLabel("Device Connection") - popup_label.setStyleSheet("color: white; font-size: 32px;") - container_layout.addWidget(popup_label) - - container_layout.setAlignment(Qt.AlignCenter) - title_layout.addWidget(title_container) - popup_layout.addLayout(title_layout) - - # 設備列表 - self.device_list_widget_popup = QListWidget(popup) - popup_layout.addWidget(self.device_list_widget_popup) - - # 按鈕區域 - button_layout = QHBoxLayout() - - refresh_button = QPushButton("Refresh") - refresh_button.clicked.connect(self.refresh_devices) - refresh_button.setFixedSize(110, 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.clicked.connect(self.hide_device_popup) - button_layout.addWidget(done_button) - - button_layout.setSpacing(10) - popup_layout.addSpacing(20) - popup_layout.addLayout(button_layout) - - self.device_popup = popup - return popup - except Exception as e: - print(f"Error in create_device_popup: {e}") return QWidget(self) - - def refresh_devices(self): - try: - print("Refreshing devices...") - device_descriptors = check_available_device() - self.connected_devices = [] - - if device_descriptors.device_descriptor_number > 0: - self.parse_and_store_devices(device_descriptors.device_descriptor_list) - self.display_devices(device_descriptors.device_descriptor_list) - else: - self.show_no_device_gif() - except Exception as e: - print(f"Error in refresh_devices: {e}") - - def parse_and_store_devices(self, devices): - try: - for device in devices: - # 如果 device.product_id 為整數,先轉成十六進位字串,然後再做 lower 和 strip 處理 - product_id = hex(device.product_id).strip().lower() # 例如 4 -> "0x4", 256 -> "0x100" - # 根據 DongleModelMap 得到型號,若無對應則回傳 "unknown" - 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 # 加入型號資訊 - } - print(device) - 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 in parse_and_store_devices: {e}") - - - def display_devices(self, device_list): - try: - if hasattr(self, 'device_list_widget'): - self.device_list_widget.clear() - for device in self.connected_devices: - self.add_device_to_list(device) - if hasattr(self, 'device_list_widget_popup'): - self.device_list_widget_popup.clear() - for device in device_list: - item_widget = QWidget() - item_layout = QHBoxLayout(item_widget) - item_layout.setContentsMargins(5, 5, 5, 5) - - icon = QSvgWidget(os.path.join(UXUI_ASSETS, "Assets_svg/ic_window_device.svg")) - icon.setFixedSize(40, 40) - item_layout.addWidget(icon) - - text_layout = QVBoxLayout() - line1_label = QLabel(f"Dongle: {device.product_id}") - line1_label.setStyleSheet("font-weight: bold; color: white;") - text_layout.addWidget(line1_label) - - line2_label = QLabel(f"KN number: {device.kn_number}") - line2_label.setStyleSheet("color: white;") - text_layout.addWidget(line2_label) - - item_layout.addLayout(text_layout) - - list_item = QListWidgetItem() - list_item.setSizeHint(item_widget.sizeHint()) - self.device_list_widget_popup.addItem(list_item) - self.device_list_widget_popup.setItemWidget(list_item, item_widget) - except Exception as e: - print(f"Error in display_devices: {e}") - - def show_no_device_gif(self): + def device_popup_mask_setup(self): try: - no_device_label = QLabel(self) - no_device_movie = QMovie(os.path.join(UXUI_ASSETS, "no_device_temp.gif")) - no_device_label.setMovie(no_device_movie) - no_device_movie.start() - no_device_label.setAlignment(Qt.AlignCenter) - self.layout.addWidget(no_device_label) - except Exception as e: - print(f"Error in show_no_device_gif: {e}") - - # ───────────────────────────── - # 自訂訊息框方法(第一個方法處理) - def show_custom_message(self, icon, title, message): - """ - 建立一個自定義的 QMessageBox,並設定文字顏色為白色及背景色 - """ - msgBox = QMessageBox(self) - msgBox.setIcon(icon) - msgBox.setWindowTitle(title) - msgBox.setText(message) - msgBox.setStyleSheet(""" - QLabel { color: white; } - QPushButton { color: white; } - QMessageBox { background-color: #2b2b2b; } - """) - msgBox.exec_() - # ───────────────────────────── - - def upload_file(self): - try: - print("開始呼叫 QFileDialog.getOpenFileName") - options = QFileDialog.Options() - file_path, _ = QFileDialog.getOpenFileName(self, "Upload File", "", "All Files (*)", options=options) - print("取得檔案路徑:", file_path) + print("Setting up popup mask") + # Add transparent overlay + self.overlay = QWidget(self) - if file_path: - print("檢查上傳目錄是否存在") - if not os.path.exists(UPLOAD_DIR): - os.makedirs(UPLOAD_DIR) - print("建立 UPLOAD_DIR:", UPLOAD_DIR) - - print("檢查原始檔案是否存在:", file_path) - if not os.path.exists(file_path): - self.show_custom_message(QMessageBox.Critical, "錯誤", "找不到選擇的檔案") - return - - file_name = os.path.basename(file_path) - self.destination = os.path.join(UPLOAD_DIR, file_name) - print("計算目標路徑:", self.destination) - - # 檢查目標路徑是否可寫 - try: - print("嘗試寫入測試檔案") - with open(self.destination, 'wb') as test_file: - pass - os.remove(self.destination) - print("測試檔案建立與刪除成功") - except PermissionError as pe: - self.show_custom_message(QMessageBox.Critical, "錯誤", "無法寫入目標資料夾") - return - - print("開始複製檔案") - shutil.copy2(file_path, self.destination) - print("檔案複製完成") - self.show_custom_message(QMessageBox.Information, "成功", f"檔案已上傳到:{self.destination}") - - except Exception as e: - import traceback - print("上傳過程中發生例外:\n", traceback.format_exc()) - self.show_custom_message(QMessageBox.Critical, "錯誤", f"上傳過程發生錯誤:{str(e)}") - - - def show_device_details(self): - print("show_device_details") - self.show_device_popup() - - # AI 模型功能實作 - def run_face_detection(self): - self.start_camera() - print("Running Face Detection") - - def run_gender_age_detection(self): - self.start_camera() - print("Running Gender/Age Detection") - - def run_object_detection(self): - self.start_camera() - print("Running Object Detection") - - def run_mask_detection(self): - self.start_camera() - print("Running Mask Detection") - - # TODO: Implement custom model upload and usage - def upload_model(self): - try: - print("Uploading Model") - options = QFileDialog.Options() - file_path, _ = QFileDialog.getOpenFileName(self, "Upload Model", "", "Model Files (*.nef);;All Files (*)", options=options) - if file_path: - if file_path.lower().endswith('.nef'): - print(f"模型已上傳:{file_path}") - # 此處添加上傳模型的處理邏輯 - else: - self.show_custom_message(QMessageBox.Critical, "錯誤", "無效的模型檔案格式。請選擇 .nef 檔案。") - except Exception as e: - print(f"Error in upload_model: {e}") - - #──────────────────────────── - def start_camera(self): - 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("相機已啟動") - # 啟動 inference worker - self.inference_worker.start() - else: - print("相機已經在運行中") - - def stop_camera(self): - if self.video_thread is not None: - self.video_thread.stop() - self.video_thread = None - print("相機已停止") - if self.inference_worker: - self.inference_worker.stop() - - #──────────────────────────── - # update_image:更新畫面並同時將 frame 推入 inference_queue - def update_image(self, qt_image): - try: - # 更新 canvas 顯示 - canvas_size = self.canvas_label.size() - scaled_image = qt_image.scaled( - canvas_size.width() - 20, - canvas_size.height() - 20, - Qt.KeepAspectRatio, - Qt.SmoothTransformation - ) - self.canvas_label.setPixmap(QPixmap.fromImage(scaled_image)) - # 轉換 QImage 成 numpy 陣列 - frame_np = qimage_to_numpy(qt_image) - # 若佇列未滿,放入最新 frame - if not self.inference_queue.full(): - self.inference_queue.put(frame_np) - else: - # 可選擇丟棄舊資料或覆蓋,這裡直接忽略新 frame - pass - except Exception as e: - print(f"Error in update_image: {e}") - - #──────────────────────────── - # 處理 inference 結果:將結果(bounding boxes、文字等)疊加在畫面上 - def handle_inference_result(self, result): - # 將結果打印到 console - print("Inference result received:", result) - - # 建立 QMessageBox - msgBox = QMessageBox(self) - msgBox.setWindowTitle("推論結果") - - # 根據 result 格式化顯示文字 - 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) - - # 設定樣式:改變 QLabel 和 QPushButton 的文字顏色為白色,並可設定背景色 - msgBox.setStyleSheet(""" - QLabel { - color: white; - } - """) - - msgBox.exec_() - - def record_video(self): - if not self.recording: - try: - self.recording = True - self.recorded_frames = [] - print("Started video recording") - - sender = self.sender() - if sender: - sender.setStyleSheet(""" - QPushButton { - background: rgba(255, 0, 0, 0.3); - border: 1px solid red; - border-radius: 10px; - } - """) - except Exception as e: - print(f"Error starting video recording: {e}") - else: - try: - self.recording = False - print("Stopped video recording") - - if self.recorded_frames: - filename = QFileDialog.getSaveFileName(self, "Save Video", "", "Video Files (*.avi)")[0] - if filename: - height, width = self.recorded_frames[0].shape[:2] - out = cv2.VideoWriter(filename, cv2.VideoWriter_fourcc(*'XVID'), 20.0, (width, height)) - for frame in self.recorded_frames: - out.write(frame) - out.release() - print(f"Video saved to {filename}") - - sender = self.sender() - if sender: - sender.setStyleSheet(""" - QPushButton { - background: transparent; - border: 1px transparent; - border-radius: 10px; - } - QPushButton:hover { - background-color: rgba(255, 255, 255, 50); - } - """) - except Exception as e: - print(f"Error stopping video recording: {e}") - - def record_audio(self): - if not self.recording_audio: - try: - self.recording_audio = True - print("Started audio recording") - - sender = self.sender() - if sender: - sender.setStyleSheet(""" - QPushButton { - background: rgba(255, 0, 0, 0.3); - border: 1px solid red; - border-radius: 10px; - } - """) - - except Exception as e: - print(f"Error starting audio recording: {e}") - else: - try: - self.recording_audio = False - print("Stopped audio recording") - - sender = self.sender() - if sender: - sender.setStyleSheet(""" - QPushButton { - background: transparent; - border: 1px transparent; - border-radius: 10px; - } - QPushButton:hover { - background-color: rgba(255, 255, 255, 50); - } - """) - except Exception as e: - print(f"Error stopping audio recording: {e}") - - def take_screenshot(self): - try: - if self.canvas_label.pixmap(): - filename = QFileDialog.getSaveFileName(self, "Save Screenshot", "", "Image Files (*.png *.jpg)")[0] - if filename: - self.canvas_label.pixmap().save(filename) - print(f"Screenshot saved to {filename}") - except Exception as e: - print(f"Error taking screenshot: {e}") - - def closeEvent(self, event): - try: - if hasattr(self, 'video_thread') and self.video_thread is not None: - self.stop_camera() + # Ensure overlay is on top + self.overlay.raise_() - if hasattr(self, 'recording') and self.recording: - self.record_video() - - if hasattr(self, 'recording_audio') and self.recording_audio: - self.record_audio() - - event.accept() + self.overlay.setStyleSheet(""" + background-color: rgba(0, 0, 0, 0.7); + """) + self.overlay.setGeometry(0, 0, self.width(), self.height()) + self.overlay_layout = QVBoxLayout(self.overlay) + + # Device connection popup + self.device_popup = create_device_popup(self, self.device_controller) + self.overlay_layout.addWidget(self.device_popup, alignment=Qt.AlignCenter) + + print("Popup window setup complete") except Exception as e: - print(f"Error during closeEvent: {e}") - event.accept() - + print(f"Error in device_popup_mask_setup: {e}") + def show_device_popup(self): try: self.overlay.show() @@ -1200,84 +176,77 @@ class MainWindow(QWidget): self.overlay.hide() except Exception as e: print(f"Error in hide_device_popup: {e}") - - def generate_global_config(self): - """掃描目錄結構並生成全局配置檔案""" + + def clear_layout(self): try: - config = {"plugins": []} - - # 確認 utils 目錄存在 - if not os.path.exists(UTILS_DIR): - os.makedirs(UTILS_DIR, exist_ok=True) - print(f"已建立 UTILS_DIR: {UTILS_DIR}") - - # 列出 utils 目錄下的所有項目以進行偵錯 - print(f"UTILS_DIR 內容: {os.listdir(UTILS_DIR) if os.path.exists(UTILS_DIR) else '目錄不存在'}") - - # 掃描模式目錄(第一層子目錄) - mode_dirs = [d for d in os.listdir(UTILS_DIR) - if os.path.isdir(os.path.join(UTILS_DIR, d)) and not d.startswith('_')] - - print(f"找到的模式目錄: {mode_dirs}") - - for mode_name in mode_dirs: - mode_path = os.path.join(UTILS_DIR, mode_name) - - mode_info = { - "mode": mode_name, - "display_name": mode_name.replace("_", " ").title(), - "models": [] - } - - # 列出該模式目錄下的所有項目以進行偵錯 - print(f"模式 {mode_name} 的內容: {os.listdir(mode_path)}") - - # 掃描模型目錄(第二層子目錄) - model_dirs = [d for d in os.listdir(mode_path) - if os.path.isdir(os.path.join(mode_path, d)) and not d.startswith('_')] - - print(f"在模式 {mode_name} 中找到的模型: {model_dirs}") - - for model_name in model_dirs: - model_path = os.path.join(mode_path, 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: - model_config = json.load(f) - - print(f"已成功讀取模型配置: {model_config_path}") - - model_summary = { - "name": model_name, - "display_name": model_config.get("display_name", model_name.replace("_", " ").title()), - "description": model_config.get("description", ""), - "compatible_devices": model_config.get("compatible_devices", []) - } - - mode_info["models"].append(model_summary) - except Exception as e: - print(f"讀取模型配置時發生錯誤 {model_config_path}: {e}") - else: - print(f"未找到模型配置檔: {model_config_path}") - # 可選:自動創建模板配置檔 - self.create_model_config_template(model_path) - - # 只添加含有模型的模式 - if mode_info["models"]: - config["plugins"].append(mode_info) - - # 寫入配置檔 - os.makedirs(os.path.dirname(SCRIPT_CONFIG), exist_ok=True) - with open(SCRIPT_CONFIG, "w", encoding="utf-8") as f: - json.dump(config, f, indent=2, ensure_ascii=False) - - print(f"全局配置已生成: {SCRIPT_CONFIG}") - return config + # Clear all widgets + while self.layout.count(): + child = self.layout.takeAt(0) + if child.widget(): + child.widget().deleteLater() except Exception as e: - print(f"生成全局配置時發生錯誤: {e}") - import traceback - traceback.print_exc() - return {"plugins": []} \ No newline at end of file + 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_() + + def show_no_device_gif(self): + """Show a GIF indicating no devices are connected""" + try: + no_device_label = QLabel(self) + no_device_movie = QMovie(os.path.join(UXUI_ASSETS, "no_device_temp.gif")) + no_device_label.setMovie(no_device_movie) + no_device_movie.start() + no_device_label.setAlignment(Qt.AlignCenter) + self.layout.addWidget(no_device_label) + except Exception as e: + print(f"Error in show_no_device_gif: {e}") + + def closeEvent(self, event): + """Handle application close event""" + try: + # Stop camera if running + self.media_controller.stop_camera() + + # Stop inference worker + self.inference_controller.stop_inference() + + # Stop recording if active + if self.media_controller.recording: + self.media_controller.record_video() + + if self.media_controller.recording_audio: + self.media_controller.record_audio() + + event.accept() + except Exception as e: + print(f"Error during closeEvent: {e}") + event.accept() + + def generate_global_config(self): + """Generate global configuration if needed""" + return self.config_utils.generate_global_config() \ No newline at end of file diff --git a/src/views/selection_screen.py b/src/views/selection_screen.py new file mode 100644 index 0000000..f9774ec --- /dev/null +++ b/src/views/selection_screen.py @@ -0,0 +1,92 @@ +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel +from PyQt5.QtCore import Qt, pyqtSignal +from PyQt5.QtGui import QPixmap, QFont +import os +from src.config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR + +class SelectionScreen(QWidget): + # Signals for navigation + open_utilities = pyqtSignal() + open_demo_app = pyqtSignal() + + def __init__(self, parent=None): + super().__init__(parent) + self.init_ui() + + def init_ui(self): + # Basic window setup + self.setGeometry(100, 100, *WINDOW_SIZE) + self.setStyleSheet(f"background-color: {BACKGROUND_COLOR};") + + # Main layout + layout = QVBoxLayout(self) + + # 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) + logo_label.setAlignment(Qt.AlignCenter) + layout.addWidget(logo_label) + + # Title + title_label = QLabel("Kneron Academy", self) + title_label.setAlignment(Qt.AlignCenter) + title_label.setFont(QFont("Arial", 24, QFont.Bold)) + layout.addWidget(title_label) + + # Subtitle + subtitle_label = QLabel("Please select an option to continue", self) + subtitle_label.setAlignment(Qt.AlignCenter) + subtitle_label.setFont(QFont("Arial", 14)) + layout.addWidget(subtitle_label) + + # Add some space + layout.addSpacing(40) + + # Button container + button_container = QWidget(self) + button_layout = QHBoxLayout(button_container) + button_layout.setContentsMargins(50, 0, 50, 0) + + # 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; + } + QPushButton:hover { + background-color: #1976D2; + } + """) + 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; + } + QPushButton:hover { + background-color: #388E3C; + } + """) + demo_button.clicked.connect(self.open_demo_app.emit) + + # Add buttons to layout + button_layout.addWidget(utilities_button) + button_layout.addWidget(demo_button) + + layout.addWidget(button_container) + layout.addStretch(1) # Push everything up diff --git a/src/views/utilities_screen.py b/src/views/utilities_screen.py new file mode 100644 index 0000000..e161fcd --- /dev/null +++ b/src/views/utilities_screen.py @@ -0,0 +1,373 @@ +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, + QFrame, QMessageBox, QScrollArea, QTableWidget, QTableWidgetItem, + QHeaderView, QProgressBar) +from PyQt5.QtCore import Qt, pyqtSignal, QTimer +from PyQt5.QtGui import QPixmap, QFont, QIcon +import os +from src.config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR +from src.controllers.device_controller import DeviceController + +class UtilitiesScreen(QWidget): + # Signals for navigation + back_to_selection = pyqtSignal() + + def __init__(self, parent=None): + super().__init__(parent) + self.device_controller = DeviceController(self) + self.init_ui() + + def init_ui(self): + # Basic window setup + self.setGeometry(100, 100, *WINDOW_SIZE) + self.setStyleSheet(f"background-color: {BACKGROUND_COLOR};") + + # Main layout + main_layout = QVBoxLayout(self) + + # Header with back button and logo + header_layout = QHBoxLayout() + + # Back button + back_button = QPushButton("", self) + back_button.setIcon(QIcon(os.path.join(UXUI_ASSETS, "Assets_png/back_arrow.png"))) + back_button.setIconSize(QPixmap(os.path.join(UXUI_ASSETS, "Assets_png/back_arrow.png")).size()) + back_button.setFixedSize(40, 40) + back_button.setStyleSheet(""" + QPushButton { + background-color: transparent; + border: none; + } + QPushButton:hover { + background-color: rgba(200, 200, 200, 0.3); + border-radius: 20px; + } + """) + back_button.clicked.connect(self.back_to_selection.emit) + + # 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) + scaled_logo = logo_pixmap.scaled(104, 40, Qt.KeepAspectRatio, Qt.SmoothTransformation) + logo_label.setPixmap(scaled_logo) + + 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) + + # Device connection section + device_section = QFrame(self) + device_section.setStyleSheet(""" + QFrame { + background-color: #f5f5f5; + border-radius: 8px; + padding: 10px; + } + """) + device_layout = QVBoxLayout(device_section) + + device_title = QLabel("Device Connection", self) + device_title.setFont(QFont("Arial", 16, QFont.Bold)) + device_layout.addWidget(device_title) + + device_subtitle = QLabel("Connect and manage your Kneron devices", self) + device_subtitle.setFont(QFont("Arial", 12)) + device_layout.addWidget(device_subtitle) + + # Device table + self.device_table = QTableWidget(0, 5, self) + self.device_table.setHorizontalHeaderLabels([ + "Device ID", "Product ID", "Firmware", "KN Number", "Status" + ]) + self.device_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + self.device_table.setStyleSheet(""" + QTableWidget { + border: none; + gridline-color: #e0e0e0; + } + QHeaderView::section { + background-color: #f0f0f0; + padding: 8px; + font-weight: bold; + border: none; + border-bottom: 1px solid #e0e0e0; + } + """) + device_layout.addWidget(self.device_table) + + # Refresh and actions buttons + device_buttons_layout = QHBoxLayout() + + refresh_button = QPushButton("Refresh Devices", self) + refresh_button.setStyleSheet(""" + QPushButton { + background-color: #42a5f5; + color: white; + border-radius: 5px; + padding: 8px 15px; + } + QPushButton:hover { + background-color: #2196f3; + } + """) + refresh_button.clicked.connect(self.refresh_devices) + + register_button = QPushButton("Register Device", self) + register_button.setStyleSheet(""" + QPushButton { + background-color: #66bb6a; + color: white; + border-radius: 5px; + padding: 8px 15px; + } + QPushButton:hover { + background-color: #4caf50; + } + """) + register_button.clicked.connect(self.register_device) + + update_fw_button = QPushButton("Update Firmware", self) + update_fw_button.setStyleSheet(""" + QPushButton { + background-color: #ffa726; + color: white; + border-radius: 5px; + padding: 8px 15px; + } + QPushButton:hover { + background-color: #ff9800; + } + """) + update_fw_button.clicked.connect(self.update_firmware) + + device_buttons_layout.addWidget(refresh_button) + device_buttons_layout.addWidget(register_button) + device_buttons_layout.addWidget(update_fw_button) + + device_layout.addLayout(device_buttons_layout) + + # Add device section to content + content_layout.addWidget(device_section) + + # Status section + status_section = QFrame(self) + status_section.setStyleSheet(""" + QFrame { + background-color: #f5f5f5; + border-radius: 8px; + padding: 10px; + margin-top: 15px; + } + """) + status_layout = QVBoxLayout(status_section) + + status_title = QLabel("Device Status", self) + status_title.setFont(QFont("Arial", 16, QFont.Bold)) + status_layout.addWidget(status_title) + + # Current status + self.status_label = QLabel("No devices connected", self) + self.status_label.setFont(QFont("Arial", 12)) + status_layout.addWidget(self.status_label) + + # Progress bar for operations + self.progress_section = QFrame(self) + self.progress_section.setVisible(False) + progress_layout = QVBoxLayout(self.progress_section) + + self.progress_title = QLabel("Operation in progress...", self) + 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) + 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) + + def refresh_devices(self): + """Refresh the list of devices""" + try: + # Clear the table + self.device_table.setRowCount(0) + + # Show progress + self.show_progress("Scanning for devices...", 0) + + # Get the devices + device_descriptors = self.device_controller.get_devices() + + # Update progress + self.update_progress(50) + + # Display the devices in the table + if hasattr(device_descriptors, 'device_descriptor_list'): + 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 + product_id = QTableWidgetItem(str(device.get("product_id", "-"))) + self.device_table.setItem(i, 1, product_id) + + # Firmware + firmware = QTableWidgetItem(str(device.get("firmware", "-"))) + self.device_table.setItem(i, 2, firmware) + + # KN Number + kn_number = QTableWidgetItem(str(device.get("kn_number", "-"))) + self.device_table.setItem(i, 3, kn_number) + + # Status + status = QTableWidgetItem("Connected" if device.get("is_connectable", False) else "Not Available") + self.device_table.setItem(i, 4, 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)") + else: + self.status_label.setText("No devices found") + + except Exception as 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)}") + + 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") + 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)) + + 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") + 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) + + # Simulate download process + for i in range(1, 5): + QTimer.singleShot(i * 500, lambda val=i*25: self.update_progress(val)) + + # Simulate installation + QTimer.singleShot(2500, lambda: self.show_progress("Installing firmware...", 50)) + + for i in range(1, 5): + QTimer.singleShot(2500 + i * 500, lambda val=50+i*10: self.update_progress(val)) + + # Simulate completion + QTimer.singleShot(5000, lambda: self.firmware_update_complete(True)) + + def show_progress(self, title, value): + """Show the progress bar with the given title and value""" + self.progress_title.setText(title) + self.progress_bar.setValue(value) + self.progress_section.setVisible(True) + + def update_progress(self, value): + """Update the progress bar value""" + self.progress_bar.setValue(value) + + def hide_progress(self): + """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 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.")