KNEO-Academy/src/views/utilities_screen.py
abin 7e323cf3e1 Fix: resolve 5 bugs found during project onboarding health check
- 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>
2026-04-07 14:33:37 +08:00

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"