- custom_inference_worker: reuse existing device_group from DeviceController to avoid double kp.connect_devices() conflict on same USB port - custom_inference_worker: add TYPE_CHECKING guard for kp type annotations to prevent potential NameError at import time - utilities_screen: replace missing back_arrow.png with text arrow (←) - utilities_screen: add set_device_controller() so AppController can inject MainWindow's shared DeviceController instance - main.py: wire UtilitiesScreen to share MainWindow's DeviceController - video_thread: emit camera_error_signal on failure and max-retry exhaustion - media_controller: connect camera_error_signal and display error on canvas - media_panel: fix pause button using wrong delete icon; use video_normal SVG Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1227 lines
45 KiB
Python
1227 lines
45 KiB
Python
"""
|
|
utilities_screen.py - Device Utilities Screen
|
|
|
|
This module contains the UtilitiesScreen class which provides device management
|
|
functionality including device scanning, firmware updates, driver installation,
|
|
and purchased items management.
|
|
"""
|
|
|
|
from PyQt5.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
|
|
QFrame, QMessageBox, QScrollArea, QTableWidget, QTableWidgetItem,
|
|
QHeaderView, QProgressBar, QLineEdit, QAbstractItemView
|
|
)
|
|
from PyQt5.QtCore import Qt, pyqtSignal, QTimer
|
|
from PyQt5.QtGui import QPixmap, QFont, QIcon, QColor
|
|
import os
|
|
from src.config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR, DongleModelMap
|
|
from src.controllers.device_controller import DeviceController
|
|
from src.services.device_service import check_available_device
|
|
from ..config import FW_DIR
|
|
|
|
|
|
class UtilitiesScreen(QWidget):
|
|
"""
|
|
Utilities Screen Class
|
|
|
|
Provides device management functionality with two main pages:
|
|
1. Utilities Page: Device connection, firmware updates, driver installation
|
|
2. Purchased Items Page: Download and manage purchased AI models
|
|
|
|
Signals:
|
|
back_to_selection: Emitted when user clicks back button
|
|
|
|
Attributes:
|
|
device_controller (DeviceController): Controller for device operations
|
|
current_page (str): Current page being displayed ("utilities" or "purchased_items")
|
|
device_table (QTableWidget): Table displaying connected devices
|
|
purchased_table (QTableWidget): Table displaying purchased items
|
|
progress_bar (QProgressBar): Progress bar for operations
|
|
status_label (QLabel): Status message display
|
|
"""
|
|
|
|
# Signals for navigation
|
|
back_to_selection = pyqtSignal()
|
|
|
|
def __init__(self, parent=None):
|
|
"""
|
|
Initialize the UtilitiesScreen.
|
|
|
|
Args:
|
|
parent: Optional parent widget.
|
|
"""
|
|
super().__init__(parent)
|
|
self.device_controller = DeviceController(self)
|
|
self.current_page = "utilities" # Track current page: "utilities" or "purchased_items"
|
|
self.init_ui()
|
|
|
|
def set_device_controller(self, device_controller):
|
|
"""
|
|
Replace the local DeviceController with a shared instance.
|
|
|
|
Call this from AppController after all screens are created so that
|
|
UtilitiesScreen and MainWindow share the same device connection state.
|
|
|
|
Args:
|
|
device_controller: Shared DeviceController instance from MainWindow.
|
|
"""
|
|
self.device_controller = device_controller
|
|
|
|
def init_ui(self):
|
|
"""
|
|
Initialize the user interface.
|
|
|
|
Creates the main layout with:
|
|
- Header with navigation buttons and logo
|
|
- Content container with switchable pages (utilities and purchased items)
|
|
"""
|
|
# Basic window setup
|
|
self.setGeometry(100, 100, *WINDOW_SIZE)
|
|
self.setStyleSheet("background-color: #F5F7FA;")
|
|
|
|
# Main layout
|
|
self.main_layout = QVBoxLayout(self)
|
|
self.main_layout.setContentsMargins(20, 20, 20, 20)
|
|
self.main_layout.setSpacing(20)
|
|
|
|
# Header with back button and logo
|
|
header_frame = self.create_header()
|
|
self.main_layout.addWidget(header_frame)
|
|
|
|
# Create main content container
|
|
self.content_container = QFrame(self)
|
|
self.content_container.setStyleSheet("""
|
|
QFrame {
|
|
background-color: white;
|
|
border-radius: 10px;
|
|
border: 1px solid #E0E0E0;
|
|
}
|
|
""")
|
|
content_layout = QVBoxLayout(self.content_container)
|
|
content_layout.setContentsMargins(20, 20, 20, 20)
|
|
content_layout.setSpacing(20)
|
|
|
|
# Create containers for both pages
|
|
self.utilities_page = QWidget()
|
|
self.purchased_items_page = QWidget()
|
|
|
|
# Set up utilities page
|
|
self.setup_utilities_page()
|
|
|
|
# Set up purchased items page
|
|
self.setup_purchased_items_page()
|
|
|
|
# Add pages to content container
|
|
content_layout.addWidget(self.utilities_page)
|
|
content_layout.addWidget(self.purchased_items_page)
|
|
|
|
# Initially show utilities page
|
|
self.utilities_page.show()
|
|
self.purchased_items_page.hide()
|
|
|
|
# Add content container to main layout
|
|
self.main_layout.addWidget(self.content_container, 1)
|
|
|
|
# Note: Auto-refresh disabled to prevent blocking
|
|
# QTimer.singleShot(500, self.refresh_devices)
|
|
|
|
def create_header(self):
|
|
"""
|
|
Create the header section with navigation elements.
|
|
|
|
Builds a header frame containing:
|
|
- Back button for returning to selection screen
|
|
- Title label showing current page
|
|
- Navigation buttons for switching between Utilities and Purchased Items pages
|
|
- Kneron logo on the right side
|
|
|
|
Returns:
|
|
QFrame: The header frame widget.
|
|
"""
|
|
header_frame = QFrame(self)
|
|
header_frame.setStyleSheet("background-color: #2C3E50; border-radius: 0px;")
|
|
header_frame.setFixedHeight(60)
|
|
|
|
header_layout = QHBoxLayout(header_frame)
|
|
header_layout.setContentsMargins(20, 0, 20, 0)
|
|
|
|
# Back button
|
|
back_button = QPushButton("←", self)
|
|
back_button.setFixedSize(40, 40)
|
|
back_button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: transparent;
|
|
border: none;
|
|
color: white;
|
|
font-size: 20px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: rgba(255, 255, 255, 0.1);
|
|
border-radius: 20px;
|
|
}
|
|
""")
|
|
back_button.clicked.connect(self.back_to_selection.emit)
|
|
header_layout.addWidget(back_button, alignment=Qt.AlignLeft)
|
|
|
|
# Title
|
|
self.title_label = QLabel("Utilities", self)
|
|
self.title_label.setStyleSheet("color: white; font-size: 24px; font-weight: bold;")
|
|
header_layout.addWidget(self.title_label, alignment=Qt.AlignCenter)
|
|
|
|
# Navigation buttons
|
|
nav_container = QFrame()
|
|
nav_layout = QHBoxLayout(nav_container)
|
|
nav_layout.setContentsMargins(0, 0, 0, 0)
|
|
nav_layout.setSpacing(10)
|
|
|
|
# Utilities button
|
|
self.utilities_button = QPushButton("Utilities", self)
|
|
self.utilities_button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #3498DB;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
padding: 5px 10px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #2980B9;
|
|
}
|
|
QPushButton:disabled {
|
|
background-color: #3498DB;
|
|
color: white;
|
|
}
|
|
""")
|
|
self.utilities_button.clicked.connect(self.show_utilities_page)
|
|
nav_layout.addWidget(self.utilities_button)
|
|
|
|
# Purchased Items button
|
|
self.purchased_items_button = QPushButton("Purchased Items", self)
|
|
self.purchased_items_button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: transparent;
|
|
color: #BDC3C7;
|
|
border: none;
|
|
border-radius: 5px;
|
|
padding: 5px 10px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
color: white;
|
|
}
|
|
QPushButton:disabled {
|
|
background-color: #3498DB;
|
|
color: white;
|
|
}
|
|
""")
|
|
self.purchased_items_button.clicked.connect(self.show_purchased_items_page)
|
|
nav_layout.addWidget(self.purchased_items_button)
|
|
|
|
header_layout.addWidget(nav_container)
|
|
|
|
# Logo
|
|
logo_label = QLabel(self)
|
|
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(logo_label, alignment=Qt.AlignRight)
|
|
|
|
return header_frame
|
|
|
|
def setup_utilities_page(self):
|
|
"""
|
|
Set up the utilities page layout.
|
|
|
|
Creates the utilities page with:
|
|
- Device connection section for managing connected devices
|
|
- Status section showing operation progress and device status
|
|
"""
|
|
utilities_layout = QVBoxLayout(self.utilities_page)
|
|
utilities_layout.setContentsMargins(0, 0, 0, 0)
|
|
utilities_layout.setSpacing(20)
|
|
|
|
# Device connection section
|
|
device_section = self.create_device_section()
|
|
utilities_layout.addWidget(device_section)
|
|
|
|
# Status section
|
|
status_section = self.create_status_section()
|
|
utilities_layout.addWidget(status_section)
|
|
|
|
def setup_purchased_items_page(self):
|
|
"""
|
|
Set up the purchased items page layout.
|
|
|
|
Creates the page for displaying and downloading purchased AI models
|
|
and packages from the Kneron store.
|
|
"""
|
|
purchased_items_layout = QVBoxLayout(self.purchased_items_page)
|
|
purchased_items_layout.setContentsMargins(0, 0, 0, 0)
|
|
purchased_items_layout.setSpacing(20)
|
|
|
|
# Purchased items section
|
|
purchased_items_section = self.create_purchased_items_section()
|
|
purchased_items_layout.addWidget(purchased_items_section)
|
|
|
|
def create_purchased_items_section(self):
|
|
"""
|
|
Create the purchased items section.
|
|
|
|
Returns:
|
|
QFrame: A frame containing the purchased items table and action buttons.
|
|
"""
|
|
purchased_section = QFrame()
|
|
purchased_section.setStyleSheet("""
|
|
QFrame {
|
|
background-color: white;
|
|
border-radius: 8px;
|
|
border: 1px solid #E0E0E0;
|
|
}
|
|
""")
|
|
|
|
purchased_layout = QVBoxLayout(purchased_section)
|
|
purchased_layout.setContentsMargins(15, 15, 15, 15)
|
|
purchased_layout.setSpacing(15)
|
|
|
|
# Title
|
|
title_label = QLabel("Your Purchased Items", purchased_section)
|
|
title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #2C3E50;")
|
|
purchased_layout.addWidget(title_label)
|
|
|
|
# Description
|
|
desc_label = QLabel("Select items to download to your device", purchased_section)
|
|
desc_label.setStyleSheet("font-size: 14px; color: #7F8C8D;")
|
|
purchased_layout.addWidget(desc_label)
|
|
|
|
# Items table (5 columns, "Action" column removed)
|
|
self.purchased_table = QTableWidget()
|
|
self.purchased_table.setColumnCount(5)
|
|
self.purchased_table.setHorizontalHeaderLabels([
|
|
"Select", "Product", "Model", "Current Version", "Compatible Dongles"
|
|
])
|
|
|
|
# Set column widths
|
|
header = self.purchased_table.horizontalHeader()
|
|
header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # Checkbox column
|
|
header.setSectionResizeMode(1, QHeaderView.Stretch)
|
|
header.setSectionResizeMode(2, QHeaderView.Stretch)
|
|
header.setSectionResizeMode(3, QHeaderView.ResizeToContents)
|
|
header.setSectionResizeMode(4, QHeaderView.Stretch)
|
|
|
|
# Set table height
|
|
self.purchased_table.setMinimumHeight(300)
|
|
|
|
# Enable row selection
|
|
self.purchased_table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
|
|
self.purchased_table.setStyleSheet("""
|
|
QTableWidget {
|
|
background-color: white;
|
|
color: #2C3E50;
|
|
border: 1px solid #E0E0E0;
|
|
border-radius: 8px;
|
|
gridline-color: #E0E0E0;
|
|
}
|
|
QTableWidget::item {
|
|
padding: 8px;
|
|
border-bottom: 1px solid #E0E0E0;
|
|
}
|
|
QTableWidget::item:selected {
|
|
background-color: #3498DB;
|
|
color: white;
|
|
}
|
|
QHeaderView::section {
|
|
background-color: #3498DB;
|
|
color: white;
|
|
padding: 8px;
|
|
border: none;
|
|
font-weight: bold;
|
|
}
|
|
""")
|
|
purchased_layout.addWidget(self.purchased_table)
|
|
|
|
# Add mock data for demonstration
|
|
self.populate_mock_purchased_items()
|
|
|
|
# Download buttons
|
|
button_layout = QHBoxLayout()
|
|
button_layout.setSpacing(10)
|
|
|
|
refresh_button = QPushButton("Refresh Items", purchased_section)
|
|
refresh_button.setMinimumHeight(40)
|
|
refresh_button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #3498DB;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
padding: 10px 15px;
|
|
font-weight: bold;
|
|
font-size: 14px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #2980B9;
|
|
}
|
|
QPushButton:pressed {
|
|
background-color: #1F618D;
|
|
}
|
|
""")
|
|
refresh_button.clicked.connect(self.populate_mock_purchased_items)
|
|
button_layout.addWidget(refresh_button)
|
|
|
|
download_button = QPushButton("Download Selected Items", purchased_section)
|
|
download_button.setMinimumHeight(40)
|
|
download_button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #2ECC71;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
padding: 10px 15px;
|
|
font-weight: bold;
|
|
font-size: 14px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #27AE60;
|
|
}
|
|
QPushButton:pressed {
|
|
background-color: #1E8449;
|
|
}
|
|
""")
|
|
download_button.clicked.connect(self.download_selected_items)
|
|
button_layout.addWidget(download_button)
|
|
|
|
purchased_layout.addLayout(button_layout)
|
|
|
|
return purchased_section
|
|
|
|
def populate_mock_purchased_items(self):
|
|
"""
|
|
Populate the purchased items table with mock data.
|
|
|
|
Clears the existing table and fills it with sample purchased items
|
|
for demonstration purposes. In production, this would fetch real
|
|
data from the server.
|
|
"""
|
|
# Clear the table
|
|
self.purchased_table.setRowCount(0)
|
|
|
|
# Mock data for demonstration
|
|
mock_items = [
|
|
{
|
|
"product": "KL720 AI Package",
|
|
"model": "Face Detection",
|
|
"version": "1.2.3",
|
|
"dongles": "KL720, KL730"
|
|
},
|
|
{
|
|
"product": "KL520 AI Package",
|
|
"model": "Object Detection",
|
|
"version": "2.0.1",
|
|
"dongles": "KL520"
|
|
},
|
|
{
|
|
"product": "KL720 AI Package",
|
|
"model": "Pose Estimation",
|
|
"version": "1.5.0",
|
|
"dongles": "KL720, KL730, KL830"
|
|
},
|
|
{
|
|
"product": "KL630 AI Package",
|
|
"model": "Image Classification",
|
|
"version": "3.1.2",
|
|
"dongles": "KL630, KL720"
|
|
},
|
|
{
|
|
"product": "KL830 AI Package",
|
|
"model": "Semantic Segmentation",
|
|
"version": "1.0.0",
|
|
"dongles": "KL830"
|
|
}
|
|
]
|
|
|
|
# Add data to table
|
|
for i, item in enumerate(mock_items):
|
|
self.purchased_table.insertRow(i)
|
|
|
|
# Create checkbox item
|
|
checkbox_item = QTableWidgetItem()
|
|
checkbox_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsSelectable)
|
|
checkbox_item.setCheckState(Qt.Unchecked)
|
|
self.purchased_table.setItem(i, 0, checkbox_item)
|
|
|
|
self.purchased_table.setItem(i, 1, QTableWidgetItem(item["product"]))
|
|
self.purchased_table.setItem(i, 2, QTableWidgetItem(item["model"]))
|
|
self.purchased_table.setItem(i, 3, QTableWidgetItem(item["version"]))
|
|
self.purchased_table.setItem(i, 4, QTableWidgetItem(item["dongles"]))
|
|
|
|
def download_item(self, row):
|
|
"""
|
|
Download a specific item from the purchased items list.
|
|
|
|
Args:
|
|
row (int): The row index of the item to download.
|
|
"""
|
|
product = self.purchased_table.item(row, 1).text()
|
|
model = self.purchased_table.item(row, 2).text()
|
|
|
|
# Show progress bar
|
|
self.show_progress(f"Downloading {product} - {model}...", 0)
|
|
|
|
# Simulate download process
|
|
for i in range(1, 11):
|
|
progress = i * 10
|
|
QTimer.singleShot(i * 300, lambda p=progress: self.update_progress(p))
|
|
|
|
# Complete download
|
|
QTimer.singleShot(3000, lambda: self.handle_download_complete(product, model))
|
|
|
|
def handle_download_complete(self, product, model):
|
|
"""
|
|
Handle download completion for a single item.
|
|
|
|
Args:
|
|
product (str): The product name that was downloaded.
|
|
model (str): The model name that was downloaded.
|
|
"""
|
|
self.hide_progress()
|
|
QMessageBox.information(self, "Download Complete", f"{product} - {model} has been downloaded successfully!")
|
|
|
|
def download_selected_items(self):
|
|
"""
|
|
Download all selected/checked items from the purchased items list.
|
|
|
|
Iterates through the table to find checked items and initiates
|
|
download for each. Shows progress bar during the download process.
|
|
"""
|
|
selected_rows = set()
|
|
|
|
# Check for selected items
|
|
for row in range(self.purchased_table.rowCount()):
|
|
if self.purchased_table.item(row, 0).checkState() == Qt.Checked:
|
|
selected_rows.add(row)
|
|
|
|
if not selected_rows:
|
|
QMessageBox.warning(self, "No Selection", "Please select at least one item to download")
|
|
return
|
|
|
|
# Show progress bar
|
|
self.show_progress(f"Downloading {len(selected_rows)} items...", 0)
|
|
|
|
# Simulate download process
|
|
total_items = len(selected_rows)
|
|
for i, row in enumerate(selected_rows):
|
|
product = self.purchased_table.item(row, 1).text()
|
|
model = self.purchased_table.item(row, 2).text()
|
|
progress = int((i / total_items) * 100)
|
|
|
|
# Update progress bar
|
|
self.update_progress(progress)
|
|
self.progress_title.setText(f"Downloading {product} - {model}... ({i+1}/{total_items})")
|
|
|
|
# Simulate download delay
|
|
QTimer.singleShot((i+1) * 1000, lambda p=product, m=model: self.status_label.setText(f"Downloaded {p} - {m}"))
|
|
|
|
# Complete all downloads
|
|
QTimer.singleShot((total_items+1) * 1000, self.handle_all_downloads_complete)
|
|
|
|
def update_download_progress(self, progress, message):
|
|
"""
|
|
Update the download progress display.
|
|
|
|
Args:
|
|
progress (int): The progress percentage (0-100).
|
|
message (str): The status message to display.
|
|
"""
|
|
self.update_progress(progress)
|
|
self.progress_title.setText(message)
|
|
|
|
def handle_all_downloads_complete(self):
|
|
"""
|
|
Handle completion of all selected downloads.
|
|
|
|
Hides the progress bar and shows a success message to the user.
|
|
"""
|
|
self.hide_progress()
|
|
QMessageBox.information(self, "Downloads Complete", "All selected items have been downloaded successfully!")
|
|
|
|
def create_device_section(self):
|
|
"""
|
|
Create the device connection section.
|
|
|
|
Builds a section containing:
|
|
- Device table showing connected Kneron devices with their details
|
|
- Action buttons for refresh, register, update firmware, and install drivers
|
|
|
|
Returns:
|
|
QFrame: The device section frame widget.
|
|
"""
|
|
device_section = QFrame(self)
|
|
device_section.setStyleSheet("""
|
|
QFrame {
|
|
background-color: white;
|
|
border-radius: 8px;
|
|
border: 1px solid #E0E0E0;
|
|
}
|
|
""")
|
|
|
|
device_layout = QVBoxLayout(device_section)
|
|
device_layout.setContentsMargins(15, 15, 15, 15)
|
|
device_layout.setSpacing(15)
|
|
|
|
# Title
|
|
title_label = QLabel("Device Connection", self)
|
|
title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #2C3E50;")
|
|
device_layout.addWidget(title_label)
|
|
|
|
# Description
|
|
desc_label = QLabel("Connect and manage your Kneron devices", self)
|
|
desc_label.setStyleSheet("font-size: 14px; color: #7F8C8D;")
|
|
device_layout.addWidget(desc_label)
|
|
|
|
# Create device table
|
|
self.device_table = QTableWidget()
|
|
self.device_table.setColumnCount(6)
|
|
self.device_table.setHorizontalHeaderLabels([
|
|
"Device Type", "Port ID", "Firmware Version", "KN Number", "Link Speed", "Status"
|
|
])
|
|
|
|
# Enable row selection mode
|
|
self.device_table.setSelectionBehavior(QTableWidget.SelectRows)
|
|
self.device_table.setSelectionMode(QTableWidget.SingleSelection)
|
|
|
|
self.device_table.setStyleSheet("""
|
|
QTableWidget {
|
|
background-color: white;
|
|
color: #2C3E50;
|
|
border: 1px solid #E0E0E0;
|
|
border-radius: 8px;
|
|
gridline-color: #E0E0E0;
|
|
}
|
|
QTableWidget::item {
|
|
padding: 8px;
|
|
border-bottom: 1px solid #E0E0E0;
|
|
}
|
|
QTableWidget::item:selected {
|
|
background-color: #3498DB;
|
|
color: white;
|
|
}
|
|
QHeaderView::section {
|
|
background-color: #3498DB;
|
|
color: white;
|
|
padding: 8px;
|
|
border: none;
|
|
font-weight: bold;
|
|
}
|
|
""")
|
|
|
|
# Set header properties
|
|
header = self.device_table.horizontalHeader()
|
|
for i in range(6):
|
|
header.setSectionResizeMode(i, QHeaderView.Stretch)
|
|
|
|
# Add the table to a scroll area
|
|
table_scroll = QScrollArea()
|
|
table_scroll.setWidgetResizable(True)
|
|
table_scroll.setStyleSheet("""
|
|
QScrollArea {
|
|
border: none;
|
|
background-color: transparent;
|
|
}
|
|
""")
|
|
table_scroll.setWidget(self.device_table)
|
|
device_layout.addWidget(table_scroll)
|
|
|
|
# Connect selection changed signal
|
|
self.device_table.itemSelectionChanged.connect(self.on_device_selection_changed)
|
|
|
|
# Button layout
|
|
button_layout = QHBoxLayout()
|
|
button_layout.setSpacing(10)
|
|
|
|
# Refresh button
|
|
refresh_button = QPushButton("Refresh Devices", self)
|
|
refresh_button.setMinimumHeight(40)
|
|
refresh_button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #3498DB;
|
|
color: white;
|
|
border: 2px solid #2980B9;
|
|
border-radius: 5px;
|
|
padding: 10px 15px;
|
|
font-weight: bold;
|
|
font-size: 14px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #2980B9;
|
|
}
|
|
QPushButton:pressed {
|
|
background-color: #1F618D;
|
|
}
|
|
""")
|
|
refresh_button.clicked.connect(self.refresh_devices)
|
|
button_layout.addWidget(refresh_button)
|
|
|
|
# Register button
|
|
register_button = QPushButton("Register Device", self)
|
|
register_button.setMinimumHeight(40)
|
|
register_button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #2ECC71;
|
|
color: white;
|
|
border: 2px solid #27AE60;
|
|
border-radius: 5px;
|
|
padding: 10px 15px;
|
|
font-weight: bold;
|
|
font-size: 14px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #27AE60;
|
|
}
|
|
QPushButton:pressed {
|
|
background-color: #1E8449;
|
|
}
|
|
""")
|
|
register_button.clicked.connect(self.register_device)
|
|
button_layout.addWidget(register_button)
|
|
|
|
# Update firmware button
|
|
update_button = QPushButton("Update Firmware", self)
|
|
update_button.setMinimumHeight(40)
|
|
update_button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #F39C12;
|
|
color: white;
|
|
border: 2px solid #D35400;
|
|
border-radius: 5px;
|
|
padding: 10px 15px;
|
|
font-weight: bold;
|
|
font-size: 14px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #D35400;
|
|
}
|
|
QPushButton:pressed {
|
|
background-color: #A04000;
|
|
}
|
|
""")
|
|
update_button.clicked.connect(self.update_firmware)
|
|
button_layout.addWidget(update_button)
|
|
|
|
# Install Driver button
|
|
install_driver_button = QPushButton("Install Driver", self)
|
|
install_driver_button.setMinimumHeight(40)
|
|
install_driver_button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #9B59B6;
|
|
color: white;
|
|
border: 2px solid #8E44AD;
|
|
border-radius: 5px;
|
|
padding: 10px 15px;
|
|
font-weight: bold;
|
|
font-size: 14px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #8E44AD;
|
|
}
|
|
QPushButton:pressed {
|
|
background-color: #7D3C98;
|
|
}
|
|
""")
|
|
install_driver_button.clicked.connect(self.install_drivers)
|
|
button_layout.addWidget(install_driver_button)
|
|
|
|
device_layout.addLayout(button_layout)
|
|
|
|
return device_section
|
|
|
|
def create_status_section(self):
|
|
"""
|
|
Create the status section for displaying operation status.
|
|
|
|
Builds a section containing:
|
|
- Status label showing current device status or operation result
|
|
- Progress bar for long-running operations (hidden by default)
|
|
|
|
Returns:
|
|
QFrame: The status section frame widget.
|
|
"""
|
|
status_section = QFrame(self)
|
|
status_section.setStyleSheet("""
|
|
QFrame {
|
|
background-color: white;
|
|
border-radius: 8px;
|
|
border: 1px solid #E0E0E0;
|
|
}
|
|
""")
|
|
|
|
status_layout = QVBoxLayout(status_section)
|
|
status_layout.setContentsMargins(15, 15, 15, 15)
|
|
status_layout.setSpacing(15)
|
|
|
|
# Title
|
|
title_label = QLabel("Device Status", self)
|
|
title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #2C3E50;")
|
|
status_layout.addWidget(title_label)
|
|
|
|
# Status message
|
|
self.status_label = QLabel("No devices found", self)
|
|
self.status_label.setStyleSheet("font-size: 14px; color: #7F8C8D;")
|
|
status_layout.addWidget(self.status_label)
|
|
|
|
# Progress section
|
|
self.progress_section = QFrame(self)
|
|
self.progress_section.setVisible(False)
|
|
self.progress_section.setStyleSheet("""
|
|
QFrame {
|
|
background-color: #F8F9FA;
|
|
border-radius: 5px;
|
|
border: 1px solid #E0E0E0;
|
|
padding: 10px;
|
|
}
|
|
""")
|
|
|
|
progress_layout = QVBoxLayout(self.progress_section)
|
|
progress_layout.setContentsMargins(10, 10, 10, 10)
|
|
progress_layout.setSpacing(10)
|
|
|
|
self.progress_title = QLabel("Operation in progress...", self)
|
|
self.progress_title.setStyleSheet("font-size: 14px; font-weight: bold; color: #2C3E50;")
|
|
progress_layout.addWidget(self.progress_title)
|
|
|
|
self.progress_bar = QProgressBar(self)
|
|
self.progress_bar.setRange(0, 100)
|
|
self.progress_bar.setValue(0)
|
|
self.progress_bar.setTextVisible(True)
|
|
self.progress_bar.setStyleSheet("""
|
|
QProgressBar {
|
|
border: 1px solid #E0E0E0;
|
|
border-radius: 5px;
|
|
background-color: white;
|
|
text-align: center;
|
|
height: 20px;
|
|
}
|
|
QProgressBar::chunk {
|
|
background-color: #3498DB;
|
|
border-radius: 5px;
|
|
}
|
|
""")
|
|
progress_layout.addWidget(self.progress_bar)
|
|
|
|
status_layout.addWidget(self.progress_section)
|
|
|
|
return status_section
|
|
|
|
def refresh_devices(self):
|
|
"""
|
|
Refresh and scan for connected Kneron devices.
|
|
|
|
Clears the device table and scans for available devices using the
|
|
Kneron Plus SDK. Populates the table with device information including
|
|
model, port ID, firmware version, KN number, link speed, and status.
|
|
|
|
Returns:
|
|
bool: True if devices were found, False otherwise.
|
|
"""
|
|
try:
|
|
# Clear the table
|
|
self.device_table.setRowCount(0)
|
|
|
|
# Show progress
|
|
self.show_progress("Scanning for devices...", 0)
|
|
|
|
# Get the devices
|
|
device_descriptors = check_available_device()
|
|
|
|
# Update progress
|
|
self.update_progress(50)
|
|
|
|
# Display the devices in the table
|
|
if device_descriptors.device_descriptor_number > 0:
|
|
devices = device_descriptors.device_descriptor_list
|
|
|
|
for i, device in enumerate(devices):
|
|
self.device_table.insertRow(i)
|
|
|
|
# Product ID to Device Model mapping
|
|
product_id_hex = hex(device.product_id)
|
|
# Map the product_id to device model name using DongleModelMap
|
|
device_model = DongleModelMap.get(product_id_hex, product_id_hex)
|
|
model_item = QTableWidgetItem(device_model)
|
|
self.device_table.setItem(i, 0, model_item)
|
|
|
|
# Device ID (Port ID)
|
|
port_id = str(device.usb_port_id)
|
|
usb_id = QTableWidgetItem(port_id)
|
|
self.device_table.setItem(i, 1, usb_id)
|
|
|
|
# Try to get firmware_version from system_info
|
|
firmware_version = "-"
|
|
try:
|
|
if device.is_connectable:
|
|
import kp
|
|
# Connect to device and get system info
|
|
device_group = kp.core.connect_devices(usb_port_ids=[port_id])
|
|
system_info = kp.core.get_system_info(
|
|
device_group=device_group,
|
|
usb_port_id=device.usb_port_id
|
|
)
|
|
# Get firmware version from system_info object
|
|
if system_info and hasattr(system_info, 'firmware_version'):
|
|
# firmware_version is an object, get its string representation
|
|
fw_version = system_info.firmware_version
|
|
if hasattr(fw_version, 'firmware_version'):
|
|
# Extract version number, remove dict format
|
|
version_str = fw_version.firmware_version
|
|
# If version is in dict format, extract the value
|
|
if isinstance(version_str, dict) and 'firmware_version' in version_str:
|
|
firmware_version = version_str['firmware_version']
|
|
else:
|
|
firmware_version = version_str
|
|
else:
|
|
# Convert object to string and clean up format
|
|
version_str = str(fw_version)
|
|
# Try to extract version number from string
|
|
import re
|
|
match = re.search(r'"firmware_version":\s*"([^"]+)"', version_str)
|
|
if match:
|
|
firmware_version = match.group(1)
|
|
else:
|
|
firmware_version = version_str
|
|
except Exception as e:
|
|
print(f"Error getting firmware version: {e}")
|
|
|
|
# Firmware
|
|
firmware = QTableWidgetItem(firmware_version)
|
|
self.device_table.setItem(i, 2, firmware)
|
|
|
|
# KN Number
|
|
kn_number = QTableWidgetItem(str(device.kn_number))
|
|
self.device_table.setItem(i, 3, kn_number)
|
|
|
|
# Link Speed
|
|
link_speed_str = "Unknown"
|
|
if hasattr(device, 'link_speed'):
|
|
# Extract SPEED_XXX part from full link_speed string
|
|
full_speed = str(device.link_speed)
|
|
if "SUPER" in full_speed:
|
|
link_speed_str = "SUPER"
|
|
elif "HIGH" in full_speed:
|
|
link_speed_str = "HIGH"
|
|
elif "FULL" in full_speed:
|
|
link_speed_str = "FULL"
|
|
else:
|
|
# Try to extract KP_USB_SPEED_XXX part
|
|
import re
|
|
match = re.search(r'KP_USB_SPEED_(\w+)', full_speed)
|
|
if match:
|
|
link_speed_str = match.group(1)
|
|
|
|
link_speed = QTableWidgetItem(link_speed_str)
|
|
self.device_table.setItem(i, 4, link_speed)
|
|
|
|
# Status
|
|
status = QTableWidgetItem("Connected" if device.is_connectable else "Not Available")
|
|
status.setForeground(Qt.green if device.is_connectable else Qt.red)
|
|
self.device_table.setItem(i, 5, status)
|
|
|
|
# Hide progress
|
|
self.hide_progress()
|
|
|
|
# Update status
|
|
device_count = self.device_table.rowCount()
|
|
if device_count > 0:
|
|
self.status_label.setText(f"Found {device_count} device(s)")
|
|
self.status_label.setStyleSheet("font-size: 14px; color: #27AE60; font-weight: bold;")
|
|
else:
|
|
self.status_label.setText("No devices found")
|
|
self.status_label.setStyleSheet("font-size: 14px; color: #E74C3C;")
|
|
|
|
return device_count > 0
|
|
|
|
except Exception as e:
|
|
print(f"Error refreshing devices: {e}")
|
|
self.hide_progress()
|
|
self.status_label.setText(f"Error: {str(e)}")
|
|
self.status_label.setStyleSheet("font-size: 14px; color: #E74C3C;")
|
|
return False
|
|
|
|
def register_device(self):
|
|
"""
|
|
Register the currently selected device.
|
|
|
|
Checks if a device is selected and initiates the registration process.
|
|
Currently shows a placeholder message as this feature is planned for
|
|
a future update.
|
|
"""
|
|
selected_rows = self.device_table.selectedItems()
|
|
if not selected_rows:
|
|
QMessageBox.warning(self, "Warning", "Please select a device to register")
|
|
return
|
|
|
|
# In a real application, you would implement the device registration logic here
|
|
QMessageBox.information(self, "Info", "Device registration functionality will be implemented in a future update")
|
|
|
|
def update_firmware(self):
|
|
"""
|
|
Update firmware for the currently selected device.
|
|
|
|
Loads and flashes the SCPU and NCPU firmware files to the selected
|
|
Kneron device. Shows progress during the update process.
|
|
|
|
Raises:
|
|
Displays error message if no device is selected, firmware files
|
|
are not found, or update fails.
|
|
"""
|
|
try:
|
|
# Check if a device is selected
|
|
selected_rows = self.device_table.selectionModel().selectedRows()
|
|
if not selected_rows:
|
|
QMessageBox.warning(self, "Warning", "Please select a device to update firmware")
|
|
return
|
|
|
|
# Get selected device information
|
|
row_index = selected_rows[0].row()
|
|
device_model = self.device_table.item(row_index, 0).text() # Device model
|
|
port_id = self.device_table.item(row_index, 1).text() # Port ID
|
|
|
|
# Show progress bar
|
|
self.show_progress(f"Updating firmware for {device_model}...", 0)
|
|
|
|
# Connect to device
|
|
import kp
|
|
device_group = kp.core.connect_devices(usb_port_ids=[int(port_id)])
|
|
|
|
# Build firmware file paths
|
|
scpu_fw_path = os.path.join(FW_DIR, device_model, "fw_scpu.bin")
|
|
ncpu_fw_path = os.path.join(FW_DIR, device_model, "fw_ncpu.bin")
|
|
|
|
# Check if firmware files exist
|
|
if not os.path.exists(scpu_fw_path) or not os.path.exists(ncpu_fw_path):
|
|
self.hide_progress()
|
|
QMessageBox.critical(self, "Error", f"Firmware files not found for {device_model}")
|
|
return
|
|
|
|
# Update progress
|
|
self.update_progress(30)
|
|
|
|
# Load firmware
|
|
kp.core.load_firmware_from_file(
|
|
device_group=device_group,
|
|
scpu_fw_path=scpu_fw_path,
|
|
ncpu_fw_path=ncpu_fw_path
|
|
)
|
|
|
|
# Update progress
|
|
self.update_progress(100)
|
|
|
|
# Show success message
|
|
QMessageBox.information(self, "Success", f"Firmware for {device_model} has been updated successfully")
|
|
|
|
except Exception as e:
|
|
self.hide_progress()
|
|
QMessageBox.critical(self, "Error", f"Error updating firmware: {str(e)}")
|
|
finally:
|
|
self.hide_progress()
|
|
|
|
def install_drivers(self):
|
|
"""
|
|
Install drivers for all supported Kneron devices.
|
|
|
|
Iterates through all supported Kneron product IDs and installs
|
|
the corresponding Windows drivers. Shows progress during installation.
|
|
"""
|
|
try:
|
|
# Show progress bar
|
|
self.show_progress("Installing Kneron Device Drivers...", 0)
|
|
|
|
# List all product IDs
|
|
import kp
|
|
product_ids = [
|
|
kp.ProductId.KP_DEVICE_KL520,
|
|
kp.ProductId.KP_DEVICE_KL720_LEGACY,
|
|
kp.ProductId.KP_DEVICE_KL720,
|
|
kp.ProductId.KP_DEVICE_KL630,
|
|
kp.ProductId.KP_DEVICE_KL730,
|
|
kp.ProductId.KP_DEVICE_KL830
|
|
]
|
|
|
|
success_count = 0
|
|
total_count = len(product_ids)
|
|
|
|
# Install each driver
|
|
for i, product_id in enumerate(product_ids):
|
|
try:
|
|
# Update progress bar
|
|
progress = int((i / total_count) * 100)
|
|
self.update_progress(progress)
|
|
self.progress_title.setText(f"Installing [{product_id.name}] driver...")
|
|
|
|
# Install driver
|
|
kp.core.install_driver_for_windows(product_id=product_id)
|
|
success_count += 1
|
|
|
|
# Update status message
|
|
self.status_label.setText(f"Successfully installed {product_id.name} driver")
|
|
except kp.ApiKPException as exception:
|
|
error_msg = f"Error: install {product_id.name} driver failed, error msg: [{str(exception)}]"
|
|
self.status_label.setText(error_msg)
|
|
QMessageBox.warning(self, "Driver Installation Error", error_msg)
|
|
|
|
# Complete installation
|
|
self.update_progress(100)
|
|
self.hide_progress()
|
|
|
|
# Show result message
|
|
if success_count == total_count:
|
|
QMessageBox.information(self, "Success", "All Kneron device drivers installed successfully!")
|
|
else:
|
|
QMessageBox.information(self, "Partial Success",
|
|
f"Installed {success_count} out of {total_count} drivers. Check the status for details.")
|
|
|
|
except Exception as e:
|
|
self.hide_progress()
|
|
error_msg = f"Error during driver installation: {str(e)}"
|
|
self.status_label.setText(error_msg)
|
|
QMessageBox.critical(self, "Error", error_msg)
|
|
|
|
def show_progress(self, title, value):
|
|
"""
|
|
Show the progress section with the specified title and initial value.
|
|
|
|
Args:
|
|
title (str): The title/message to display above the progress bar.
|
|
value (int): The initial progress value (0-100).
|
|
"""
|
|
self.progress_title.setText(title)
|
|
self.progress_bar.setValue(value)
|
|
self.progress_section.setVisible(True)
|
|
|
|
def update_progress(self, value):
|
|
"""
|
|
Update the progress bar to the specified value.
|
|
|
|
Args:
|
|
value (int): The progress value (0-100).
|
|
"""
|
|
self.progress_bar.setValue(value)
|
|
|
|
def hide_progress(self):
|
|
"""Hide the progress section and reset it to default state."""
|
|
self.progress_section.setVisible(False)
|
|
|
|
def on_device_selection_changed(self):
|
|
"""
|
|
Handle device selection changes in the device table.
|
|
|
|
Updates the status label to show the currently selected device
|
|
information and ensures the entire row is highlighted.
|
|
"""
|
|
selected_rows = self.device_table.selectionModel().selectedRows()
|
|
if selected_rows:
|
|
# Get the selected row index
|
|
row_index = selected_rows[0].row()
|
|
|
|
# Update the status label to show which device is selected
|
|
device_model = self.device_table.item(row_index, 0).text()
|
|
device_id = self.device_table.item(row_index, 1).text()
|
|
self.status_label.setText(f"Selected device: {device_model} (ID: {device_id})")
|
|
|
|
# Ensure the entire row is highlighted
|
|
self.device_table.selectRow(row_index)
|
|
|
|
def show_utilities_page(self):
|
|
"""
|
|
Switch to the utilities page view.
|
|
|
|
Updates button styles to show utilities as active and purchased items
|
|
as inactive. Shows the utilities page and hides the purchased items page.
|
|
"""
|
|
self.utilities_button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #3498DB;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
padding: 5px 10px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #2980B9;
|
|
}
|
|
QPushButton:disabled {
|
|
background-color: #3498DB;
|
|
color: white;
|
|
}
|
|
""")
|
|
self.purchased_items_button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: transparent;
|
|
color: #BDC3C7;
|
|
border: none;
|
|
border-radius: 5px;
|
|
padding: 5px 10px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
color: white;
|
|
}
|
|
QPushButton:disabled {
|
|
background-color: #3498DB;
|
|
color: white;
|
|
}
|
|
""")
|
|
self.utilities_page.show()
|
|
self.purchased_items_page.hide()
|
|
self.title_label.setText("Utilities")
|
|
self.current_page = "utilities"
|
|
|
|
def show_purchased_items_page(self):
|
|
"""
|
|
Switch to the purchased items page view.
|
|
|
|
Updates button styles to show purchased items as active and utilities
|
|
as inactive. Shows the purchased items page and hides the utilities page.
|
|
"""
|
|
self.purchased_items_button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #3498DB;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
padding: 5px 10px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #2980B9;
|
|
}
|
|
QPushButton:disabled {
|
|
background-color: #3498DB;
|
|
color: white;
|
|
}
|
|
""")
|
|
self.utilities_button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: transparent;
|
|
color: #BDC3C7;
|
|
border: none;
|
|
border-radius: 5px;
|
|
padding: 5px 10px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
color: white;
|
|
}
|
|
QPushButton:disabled {
|
|
background-color: #3498DB;
|
|
color: white;
|
|
}
|
|
""")
|
|
self.utilities_page.hide()
|
|
self.purchased_items_page.show()
|
|
self.title_label.setText("Purchased Items")
|
|
self.current_page = "purchased_items"
|