Modulize the code structure & add utilites' UI element
This commit is contained in:
parent
af5b3835d5
commit
d58cec837b
72
main.py
72
main.py
@ -1,13 +1,75 @@
|
|||||||
import sys
|
import sys
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication, QStackedWidget
|
||||||
from src.views.mainWindows import MainWindow
|
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
|
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():
|
def main():
|
||||||
app = QApplication(sys.argv)
|
controller = AppController()
|
||||||
window = MainWindow()
|
return controller.run()
|
||||||
window.show()
|
|
||||||
sys.exit(app.exec_())
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@ -1,6 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
import os
|
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 為絕對路徑
|
# 取得專案根目錄的絕對路徑並設定 UXUI_ASSETS 為絕對路徑
|
||||||
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
UXUI_ASSETS = os.path.join(PROJECT_ROOT, "uxui", "")
|
UXUI_ASSETS = os.path.join(PROJECT_ROOT, "uxui", "")
|
||||||
|
|||||||
@ -1,32 +1,75 @@
|
|||||||
import kp
|
# src/controllers/device_controller.py
|
||||||
from typing import List, Dict
|
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:
|
class DeviceController:
|
||||||
def __init__(self):
|
def __init__(self, main_window):
|
||||||
|
self.main_window = main_window
|
||||||
|
self.selected_device = None
|
||||||
self.connected_devices = []
|
self.connected_devices = []
|
||||||
|
|
||||||
def scan_devices(self):
|
def refresh_devices(self):
|
||||||
return kp.core.scan_devices()
|
"""Refresh the list of connected devices"""
|
||||||
|
try:
|
||||||
|
print("Refreshing devices...")
|
||||||
|
device_descriptors = check_available_device()
|
||||||
|
self.connected_devices = []
|
||||||
|
|
||||||
def connect_device(self, usb_port_id: int):
|
if device_descriptors.device_descriptor_number > 0:
|
||||||
device_group = kp.core.connect_devices(usb_port_ids=[usb_port_id])
|
self.parse_and_store_devices(device_descriptors.device_descriptor_list)
|
||||||
kp.core.set_timeout(device_group=device_group, milliseconds=5000)
|
self.display_devices(device_descriptors.device_descriptor_list)
|
||||||
return device_group
|
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 load_firmware(self, device_group, product_id: int):
|
def parse_and_store_devices(self, devices):
|
||||||
SCPU_FW_PATH = f'../../external/res/firmware/{product_id}/fw_scpu.bin'
|
"""Parse device information and store it"""
|
||||||
NCPU_FW_PATH = f'../../external/res/firmware/{product_id}/fw_ncpu.bin'
|
for device in devices:
|
||||||
kp.core.load_firmware_from_file(
|
product_id = hex(device.product_id).strip().lower()
|
||||||
device_group=device_group,
|
dongle = DongleModelMap.get(product_id, "unknown")
|
||||||
scpu_fw_path=SCPU_FW_PATH,
|
device.dongle = dongle
|
||||||
ncpu_fw_path=NCPU_FW_PATH
|
|
||||||
)
|
|
||||||
|
|
||||||
def parse_device_info(self, device) -> Dict:
|
new_device = {
|
||||||
return {
|
'usb_port_id': device.usb_port_id,
|
||||||
'usb_port_id': device.usb_port_id,
|
'product_id': device.product_id,
|
||||||
'product_id': device.product_id,
|
'kn_number': device.kn_number,
|
||||||
'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)
|
||||||
|
|
||||||
|
# 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;")
|
||||||
139
src/controllers/inference_controller.py
Normal file
139
src/controllers/inference_controller.py
Normal file
@ -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
|
||||||
166
src/controllers/media_controller.py
Normal file
166
src/controllers/media_controller.py
Normal file
@ -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}")
|
||||||
73
src/models/inference_worker.py
Normal file
73
src/models/inference_worker.py
Normal file
@ -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()
|
||||||
30
src/models/video_thread.py
Normal file
30
src/models/video_thread.py
Normal file
@ -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()
|
||||||
@ -1,45 +1,69 @@
|
|||||||
import kp
|
# 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()
|
|
||||||
|
|
||||||
# def check_available_device():
|
# def check_available_device():
|
||||||
# print("checking available devices")
|
# try:
|
||||||
# # 模擬設備描述符
|
# print("checking available devices")
|
||||||
# device_descriptors = [
|
# device_descriptors = kp.core.scan_devices()
|
||||||
# {
|
# return device_descriptors
|
||||||
# "usb_port_id": 4,
|
# except Exception as e:
|
||||||
# "vendor_id": "0x3231",
|
# print(f"Error scanning devices: {e}")
|
||||||
# "product_id": "0x720",
|
# # 返回一個空的設備描述符或模擬數據
|
||||||
# "link_speed": "UsbSpeed.KP_USB_SPEED_SUPER",
|
# class EmptyDescriptor:
|
||||||
# "kn_number": "0xB306224C",
|
# def __init__(self):
|
||||||
# "is_connectable": True,
|
# self.device_descriptor_number = 0
|
||||||
# "usb_port_path": "4-1",
|
# self.device_descriptor_list = []
|
||||||
# "firmware": "KDP2 Comp/F"
|
# return EmptyDescriptor()
|
||||||
# },
|
|
||||||
# {
|
def check_available_device():
|
||||||
# "usb_port_id": 5,
|
# 模擬設備描述符
|
||||||
# "vendor_id": "0x3231",
|
print("checking available devices")
|
||||||
# "product_id": "0x520",
|
class EmptyDescriptor:
|
||||||
# "link_speed": "UsbSpeed.KP_USB_SPEED_SUPER",
|
def __init__(self):
|
||||||
# "kn_number": "0xB306224C",
|
self.device_descriptor_number = 0
|
||||||
# "is_connectable": True,
|
self.device_descriptor_list = [{
|
||||||
# "usb_port_path": "4-1",
|
"usb_port_id": 4,
|
||||||
# "firmware": "KDP2 Comp/F"
|
"vendor_id": "0x3231",
|
||||||
# }
|
"product_id": "0x720",
|
||||||
# ]
|
"link_speed": "UsbSpeed.KP_USB_SPEED_SUPER",
|
||||||
# return device_descriptors
|
"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):
|
# def get_dongle_type(self, product_id):
|
||||||
# for dongle_type in self.K_:
|
# for dongle_type in self.K_:
|
||||||
|
|||||||
77
src/services/file_service.py
Normal file
77
src/services/file_service.py
Normal file
@ -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_()
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import kp
|
# import kp
|
||||||
import cv2, os, shutil, sys
|
import cv2, os, shutil, sys
|
||||||
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QLabel, QPushButton,
|
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QLabel, QPushButton,
|
||||||
QComboBox, QFileDialog, QMessageBox, QHBoxLayout, QDialog, QListWidget,
|
QComboBox, QFileDialog, QMessageBox, QHBoxLayout, QDialog, QListWidget,
|
||||||
|
|||||||
116
src/utils/config_utils.py
Normal file
116
src/utils/config_utils.py
Normal file
@ -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
|
||||||
12
src/utils/image_utils.py
Normal file
12
src/utils/image_utils.py
Normal file
@ -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
|
||||||
23
src/views/components/canvas_area.py
Normal file
23
src/views/components/canvas_area.py
Normal file
@ -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)
|
||||||
60
src/views/components/device_list.py
Normal file
60
src/views/components/device_list.py
Normal file
@ -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)
|
||||||
76
src/views/components/device_popup.py
Normal file
76
src/views/components/device_popup.py
Normal file
@ -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)
|
||||||
64
src/views/components/media_panel.py
Normal file
64
src/views/components/media_panel.py
Normal file
@ -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)
|
||||||
82
src/views/components/toolbox.py
Normal file
82
src/views/components/toolbox.py
Normal file
@ -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)
|
||||||
157
src/views/login_screen.py
Normal file
157
src/views/login_screen.py
Normal file
@ -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()
|
||||||
File diff suppressed because it is too large
Load Diff
92
src/views/selection_screen.py
Normal file
92
src/views/selection_screen.py
Normal file
@ -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
|
||||||
373
src/views/utilities_screen.py
Normal file
373
src/views/utilities_screen.py
Normal file
@ -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.")
|
||||||
Loading…
x
Reference in New Issue
Block a user