Files
Mira_Firmware_Loader/main.py
2026-03-09 20:49:13 +07:00

1336 lines
55 KiB
Python

import sys
import os
import socket
import ipaddress
import platform
from concurrent.futures import ThreadPoolExecutor, as_completed
from PyQt6.QtWidgets import (
QApplication, QWidget, QPushButton,
QVBoxLayout, QHBoxLayout, QFileDialog, QTableWidget,
QTableWidgetItem, QLabel, QProgressBar,
QMessageBox, QGroupBox, QHeaderView, QLineEdit,
QFrame, QCheckBox, QSpinBox, QScrollArea, QSizePolicy,
QComboBox
)
from PyQt6.QtCore import (
Qt, QThread, pyqtSignal, QPropertyAnimation, QAbstractAnimation,
QParallelAnimationGroup, QRect, QTimer
)
from PyQt6.QtGui import QFont, QColor, QIcon, QAction
import datetime
from core.scanner import scan_network
from core.workers import ScanThread
from core.flash_new_worker import NewFlashThread
from core.flash_update_worker import UpdateFlashThread
from core.auto_flash_worker import AutoFlashWorker
from utils.network import _resolve_hostname, get_default_network
from utils.system import resource_path, get_machine_info, get_version
from ui.components import CollapsibleGroupBox
from ui.styles import STYLE
class App(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle(f"Mira Firmware Loader v{get_version()}")
self.setWindowIcon(QIcon(resource_path("icon.ico")))
# Data state
self.local_ip = ""
self.gateway_ip = ""
self.firmware = None
self.all_devices = [] # raw list from scanner
self.devices = [] # filtered list for table
self.flashed_macs = {} # MAC -> (ip, mac, result, timestamp)
self.scan_thread = None
info = get_machine_info()
self.local_ip = info["ip"]
self.gateway_ip = self._guess_gateway(self.local_ip)
# ── Main layout with stacked containers ──
root_layout = QVBoxLayout()
root_layout.setSpacing(0)
root_layout.setContentsMargins(0, 0, 0, 0)
# ── MAIN CONTAINER (default view) ──
self.main_container = QWidget()
layout = QVBoxLayout(self.main_container)
layout.setSpacing(3)
layout.setContentsMargins(8, 6, 8, 6)
# ── Title (single line) ──
title_row = QHBoxLayout()
title_row.setSpacing(0)
title = QLabel("⚡ Mira Firmware Loader")
title.setObjectName("title")
title_row.addWidget(title)
title_row.addStretch()
copyright_label = QLabel(f"© {datetime.datetime.now().year} Smatec — R&D Software Team. v{get_version()}")
copyright_label.setStyleSheet(
"color: #9399b2; font-size: 13px; font-weight: 500;"
)
title_row.addWidget(copyright_label)
layout.addLayout(title_row)
# ── Machine Info (single compact row) ──
info_group = CollapsibleGroupBox("🖥 Machine Info")
info_row = QHBoxLayout()
info_row.setSpacing(6)
info_row.setContentsMargins(0, 0, 0, 0)
for label, value in [("Host:", info["hostname"]), ("IP:", info["ip"]),
("OS:", info["os"]), ("MAC:", info["mac"])]:
lbl = QLabel(label)
lbl.setObjectName("info")
info_row.addWidget(lbl)
val = QLabel(value)
val.setStyleSheet("color: #cdd6f4; font-weight: bold; font-size: 11px;")
info_row.addWidget(val)
if label != "MAC:":
sep = QLabel("")
sep.setStyleSheet("color: #3d4a6b;")
info_row.addWidget(sep)
info_row.addStretch()
info_group.set_content_layout(info_row)
layout.addWidget(info_group)
# ── Firmware + Network Scan (combined compact row) ──
fw_scan_group = CollapsibleGroupBox("📦 FW & 📡 Scan")
fw_scan_layout = QVBoxLayout()
fw_scan_layout.setSpacing(6)
fw_scan_layout.setContentsMargins(4, 2, 4, 4)
# Row 1: FW selection (full width, styled card)
fw_card = QFrame()
fw_card.setStyleSheet("""
QFrame {
background-color: #13141f;
border: 1px solid #2d3748;
border-radius: 6px;
padding: 4px 8px;
}
""")
fw_card_layout = QHBoxLayout(fw_card)
fw_card_layout.setContentsMargins(8, 4, 8, 4)
fw_card_layout.setSpacing(8)
fw_icon = QLabel("📦")
fw_icon.setStyleSheet("font-size: 16px; border: none;")
fw_card_layout.addWidget(fw_icon)
fw_lbl = QLabel("Firmware:")
fw_lbl.setStyleSheet("font-weight: bold; font-size: 12px; color: #7eb8f7; border: none;")
fw_card_layout.addWidget(fw_lbl)
self.fw_label = QLabel("No firmware selected")
self.fw_label.setStyleSheet("font-size: 12px; color: #94a3b8; border: none;")
fw_card_layout.addWidget(self.fw_label, 1)
btn_fw = QPushButton("📁 Browse FW")
btn_fw.setFixedHeight(28)
btn_fw.setStyleSheet("""
QPushButton {
background-color: #2d3352; border: 1px solid #3d4a6b;
border-radius: 5px; padding: 3px 12px;
font-size: 12px; font-weight: bold; color: #e2e8f0;
}
QPushButton:hover {
background-color: #3d4a6b; border-color: #7eb8f7; color: #ffffff;
}
""")
btn_fw.clicked.connect(self.select_fw)
fw_card_layout.addWidget(btn_fw)
fw_scan_layout.addWidget(fw_card)
# Row 2: Network + Scan (full width, styled card)
scan_card = QFrame()
scan_card.setStyleSheet("""
QFrame {
background-color: #13141f;
border: 1px solid #2d3748;
border-radius: 6px;
padding: 4px 8px;
}
""")
scan_card_layout = QHBoxLayout(scan_card)
scan_card_layout.setContentsMargins(8, 4, 8, 4)
scan_card_layout.setSpacing(8)
net_icon = QLabel("📡")
net_icon.setStyleSheet("font-size: 16px; border: none;")
scan_card_layout.addWidget(net_icon)
net_lbl = QLabel("Network:")
net_lbl.setStyleSheet("font-weight: bold; font-size: 12px; color: #7eb8f7; border: none;")
scan_card_layout.addWidget(net_lbl)
self.net_input = QLineEdit(get_default_network(self.local_ip))
self.net_input.setPlaceholderText("e.g. 192.168.4.0/24")
self.net_input.setFixedHeight(28)
self.net_input.setMinimumWidth(140)
self.net_input.setMaximumWidth(200)
self.net_input.setStyleSheet("QLineEdit { font-size: 12px; padding: 2px 8px; border: 1px solid #3d4a6b; border-radius: 5px; background-color: #1a1b2e; }")
scan_card_layout.addWidget(self.net_input)
scan_card_layout.addStretch()
self.btn_scan = QPushButton("🔍 Scan LAN")
self.btn_scan.setObjectName("scan")
self.btn_scan.clicked.connect(self.scan)
self.btn_scan.setFixedHeight(28)
self.btn_scan.setFixedWidth(110)
scan_card_layout.addWidget(self.btn_scan)
fw_scan_layout.addWidget(scan_card)
# Scan progress + status (hidden by default)
self.scan_progress_bar = QProgressBar()
self.scan_progress_bar.setObjectName("scan_bar")
self.scan_progress_bar.setRange(0, 0)
self.scan_progress_bar.setFormat("")
self.scan_progress_bar.setFixedHeight(14)
self.scan_progress_bar.setVisible(False)
fw_scan_layout.addWidget(self.scan_progress_bar)
self.scan_status = QLabel("")
self.scan_status.setObjectName("info")
self.scan_status.setStyleSheet("font-size: 10px;")
fw_scan_layout.addWidget(self.scan_status)
fw_scan_group.set_content_layout(fw_scan_layout)
layout.addWidget(fw_scan_group)
# ── Device Table (MAIN area — maximized) ──
dev_group = QGroupBox("📋 Devices Found")
dev_layout = QVBoxLayout()
dev_layout.setSpacing(2)
dev_layout.setContentsMargins(4, 12, 4, 4)
self.table = QTableWidget()
self.table.setColumnCount(4)
self.table.setHorizontalHeaderLabels(["", "IP", "MAC", "Status"])
self.table.setAlternatingRowColors(True)
header = self.table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
header.resizeSection(0, 32)
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Interactive)
header.resizeSection(1, 115)
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Interactive)
header.resizeSection(2, 135)
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
self.table.verticalHeader().setVisible(False)
self.table.setSelectionBehavior(
QTableWidget.SelectionBehavior.SelectRows
)
dev_layout.addWidget(self.table)
filter_row = QHBoxLayout()
filter_row.setSpacing(4)
self.device_count_label = QLabel("Total: 0 devices")
self.device_count_label.setObjectName("info")
filter_row.addWidget(self.device_count_label)
filter_row.addStretch()
btn_history = QPushButton("📋 Flash History")
btn_history.setFixedHeight(24)
btn_history.clicked.connect(self._show_history)
btn_history.setStyleSheet("background-color: #313244; color: #cdd6f4; font-size: 11px; padding: 2px 8px;")
filter_row.addWidget(btn_history)
btn_select_all = QPushButton("☑ All")
btn_select_all.setFixedHeight(24)
btn_select_all.setStyleSheet("font-size: 11px; padding: 2px 8px;")
btn_select_all.clicked.connect(self._select_all_devices)
filter_row.addWidget(btn_select_all)
btn_deselect_all = QPushButton("☐ None")
btn_deselect_all.setFixedHeight(24)
btn_deselect_all.setStyleSheet("font-size: 11px; padding: 2px 8px;")
btn_deselect_all.clicked.connect(self._deselect_all_devices)
filter_row.addWidget(btn_deselect_all)
self.show_all_cb = QCheckBox("Show all")
self.show_all_cb.setChecked(False)
self.show_all_cb.setToolTip("Include gateway & self")
self.show_all_cb.stateChanged.connect(self._refresh_table)
filter_row.addWidget(self.show_all_cb)
dev_layout.addLayout(filter_row)
dev_group.setLayout(dev_layout)
layout.addWidget(dev_group, stretch=1)
# ── Flash Controls (collapsible, compact) ──
flash_group = CollapsibleGroupBox("🚀 Flash Controls")
flash_layout = QVBoxLayout()
flash_layout.setSpacing(6)
flash_layout.setContentsMargins(8, 4, 8, 6)
self.progress = QProgressBar()
self.progress.setFormat("%v / %m devices (%p%)")
self.progress.setFixedHeight(18)
flash_layout.addWidget(self.progress)
# Row 1: Mode + Method
mode_method_row = QHBoxLayout()
mode_method_row.setSpacing(10)
mode_lbl = QLabel("Mode:")
mode_lbl.setStyleSheet("font-size: 12px; font-weight: bold;")
mode_method_row.addWidget(mode_lbl)
self.mode_combo = QComboBox()
self.mode_combo.addItem("⚡ New Flash (Factory Reset)", "new")
self.mode_combo.addItem("🔄 Update FW (Pre-installed)", "update")
self.mode_combo.setMinimumWidth(230)
self.mode_combo.setFixedHeight(28)
self.mode_combo.setStyleSheet("""
QComboBox {
background-color: #2d3352; border: 1px solid #3d4a6b; border-radius: 4px;
padding: 2px 8px; color: #ffffff; font-size: 12px; font-weight: bold;
}
""")
self.mode_combo.currentIndexChanged.connect(self._on_mode_changed)
mode_method_row.addWidget(self.mode_combo)
sep_m = QLabel("")
sep_m.setStyleSheet("color: #3d4a6b;")
mode_method_row.addWidget(sep_m)
# Container cho method (ẩn khi Update mode)
self.method_container = QWidget()
mc_layout = QHBoxLayout(self.method_container)
mc_layout.setContentsMargins(0, 0, 0, 0)
mc_layout.setSpacing(6)
meth_lbl = QLabel("Method:")
meth_lbl.setStyleSheet("font-size: 12px; font-weight: bold;")
mc_layout.addWidget(meth_lbl)
self.method_combo = QComboBox()
self.method_combo.addItem("🌐 API (LuCI)", "api")
self.method_combo.addItem("💻 SSH", "ssh")
self.method_combo.setMinimumWidth(140)
self.method_combo.setFixedHeight(28)
self.method_combo.setStyleSheet("""
QComboBox {
background-color: #1e1e2e; border: 1px solid #3d4a6b; border-radius: 4px;
padding: 2px 8px; color: #ffffff; font-size: 12px; font-weight: bold;
}
""")
self.method_combo.currentIndexChanged.connect(self._on_method_changed)
mc_layout.addWidget(self.method_combo)
mode_method_row.addWidget(self.method_container)
# Warning label for Update Mode (inline)
self.update_warning_lbl = QLabel("⚠️ Update: SSH only, target 192.168.11.102")
self.update_warning_lbl.setStyleSheet("color: #f38ba8; font-size: 11px; font-weight: bold;")
self.update_warning_lbl.setVisible(False)
mode_method_row.addWidget(self.update_warning_lbl)
mode_method_row.addStretch()
flash_layout.addLayout(mode_method_row)
# SSH credentials (hidden by default)
self.ssh_creds_widget = QWidget()
self.ssh_creds_widget.setStyleSheet("background-color: #11121d; border-radius: 6px; border: 1px dashed #3d4a6b;")
ssh_creds_layout = QHBoxLayout(self.ssh_creds_widget)
ssh_creds_layout.setContentsMargins(10, 6, 10, 6)
ssh_creds_layout.setSpacing(8)
ssh_lbl1 = QLabel("User:")
ssh_lbl1.setStyleSheet("font-size: 11px; font-weight: bold;")
ssh_creds_layout.addWidget(ssh_lbl1)
str_qlineedit = "QLineEdit { background-color: #1a1b2e; font-size: 12px; padding: 3px; border: 1px solid #3d4a6b; color: #ffffff; }"
self.ssh_user_input = QLineEdit("root")
self.ssh_user_input.setFixedWidth(80)
self.ssh_user_input.setStyleSheet(str_qlineedit)
ssh_creds_layout.addWidget(self.ssh_user_input)
ssh_lbl2 = QLabel("Pass:")
ssh_lbl2.setStyleSheet("font-size: 11px; font-weight: bold;")
ssh_creds_layout.addWidget(ssh_lbl2)
self.ssh_pass_input = QLineEdit("admin123a")
self.ssh_pass_input.setEchoMode(QLineEdit.EchoMode.Password)
self.ssh_pass_input.setFixedWidth(100)
self.ssh_pass_input.setStyleSheet(str_qlineedit)
ssh_creds_layout.addWidget(self.ssh_pass_input)
self.set_passwd_cb = QCheckBox("Set passwd")
self.set_passwd_cb.setChecked(True)
self.set_passwd_cb.setStyleSheet("color: #94a3b8; font-size: 11px;")
self.set_passwd_cb.setToolTip("Set password before flash (passwd → admin123a)")
ssh_creds_layout.addWidget(self.set_passwd_cb)
ssh_creds_layout.addStretch()
self.ssh_creds_widget.setVisible(False)
flash_layout.addWidget(self.ssh_creds_widget)
# Row 2: Concurrent + Flash button
action_row = QHBoxLayout()
action_row.setSpacing(10)
par_lbl = QLabel("Concurrent:")
par_lbl.setStyleSheet("font-size: 13px; font-weight: bold; color: #e2e8f0;")
action_row.addWidget(par_lbl)
self.parallel_spin = QSpinBox()
self.parallel_spin.setRange(0, 100)
self.parallel_spin.setValue(10)
self.parallel_spin.setSpecialValueText("")
self.parallel_spin.setToolTip("0 = unlimited (all at once)")
self.parallel_spin.setMinimumWidth(80)
self.parallel_spin.setFixedHeight(34)
self.parallel_spin.setStyleSheet(
"QSpinBox { font-size: 14px; font-weight: bold; color: #e2e8f0; "
"background-color: #2d3352; border: 1px solid #3d4a6b; border-radius: 6px; padding: 2px 6px; } "
"QSpinBox::up-button { width: 18px; } QSpinBox::down-button { width: 18px; }"
)
action_row.addWidget(self.parallel_spin)
self.btn_flash = QPushButton("⚡ FLASH SELECTED DEVICES")
self.btn_flash.setObjectName("flash")
self.btn_flash.setFixedHeight(34)
self.btn_flash.setStyleSheet("QPushButton#flash { font-size: 13px; font-weight: bold; letter-spacing: 1px; }")
self.btn_flash.clicked.connect(self.flash_all)
action_row.addWidget(self.btn_flash, 1)
flash_layout.addLayout(action_row)
flash_group.set_content_layout(flash_layout)
layout.addWidget(flash_group)
# ── Automation Button ──
self.btn_auto = QPushButton("🤖 Auto Firmware Flash")
self.btn_auto.setObjectName("auto")
self.btn_auto.setFixedHeight(32)
self.btn_auto.setStyleSheet("""
QPushButton#auto {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #7c3aed, stop:1 #a78bfa);
border-color: #7c3aed; color: #ffffff;
font-size: 12px; font-weight: bold; letter-spacing: 1px;
border-radius: 6px;
}
QPushButton#auto:hover {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #8b5cf6, stop:1 #c4b5fd);
}
""")
self.btn_auto.clicked.connect(self._open_auto_flash)
layout.addWidget(self.btn_auto)
root_layout.addWidget(self.main_container)
# ── AUTO CONTAINER (hidden by default) ──
self.auto_container = QWidget()
self.auto_container.setVisible(False)
self._build_auto_ui()
root_layout.addWidget(self.auto_container)
self.setLayout(root_layout)
# ── Helpers ──
def _guess_gateway(self, ip):
"""Guess gateway IP (x.x.x.1) from local IP."""
try:
parts = ip.split(".")
return f"{parts[0]}.{parts[1]}.{parts[2]}.1"
except Exception:
return ""
def _info_label(self, text):
lbl = QLabel(text)
lbl.setObjectName("info")
return lbl
def _info_value(self, text):
lbl = QLabel(text)
lbl.setStyleSheet("color: #cdd6f4; font-weight: bold;")
return lbl
def _get_filtered_devices(self):
"""Return devices filtered based on show_all checkbox."""
if self.show_all_cb.isChecked():
return list(self.all_devices)
excluded = {self.local_ip, self.gateway_ip}
return [d for d in self.all_devices if d["ip"] not in excluded]
def _refresh_table(self):
"""Re-populate table based on current filter state."""
self.devices = self._get_filtered_devices()
self.table.setRowCount(len(self.devices))
for i, d in enumerate(self.devices):
mac_str = d["mac"].upper()
# Checkbox column
cb_item = QTableWidgetItem()
# If device MAC is already flashed, mark it
is_already_flashed = mac_str in self.flashed_macs
if is_already_flashed and d["status"] in ["READY", ""]:
d["status"] = "Already Flashed"
# Checkbox logic
if is_already_flashed:
cb_item.setCheckState(Qt.CheckState.Unchecked)
else:
cb_item.setCheckState(Qt.CheckState.Checked)
cb_item.setFlags(cb_item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
self.table.setItem(i, 0, cb_item)
ip_item = QTableWidgetItem(d["ip"])
mac_item = QTableWidgetItem(mac_str)
status_item = QTableWidgetItem(d["status"])
if is_already_flashed:
status_item.setForeground(QColor("#a6e3a1"))
ip_item.setForeground(QColor("#a6e3a1"))
# Dim gateway & self if showing all
if d["ip"] in {self.local_ip, self.gateway_ip}:
for item in [ip_item, mac_item, status_item]:
item.setForeground(QColor("#4a5568"))
cb_item.setCheckState(Qt.CheckState.Unchecked)
self.table.setItem(i, 1, ip_item)
self.table.setItem(i, 2, mac_item)
self.table.setItem(i, 3, status_item)
total = len(self.devices)
hidden = len(self.all_devices) - total
label = f"Total: {total} devices"
if hidden > 0:
label += f" (hiding {hidden})"
self.device_count_label.setText(label)
def _select_all_devices(self):
"""Check all device checkboxes."""
for i in range(self.table.rowCount()):
mac = self.table.item(i, 2).text().strip()
# Prevent checking if already flashed or is a gateway (optional but good UI logic)
if mac not in self.flashed_macs:
item = self.table.item(i, 0)
if item:
item.setCheckState(Qt.CheckState.Checked)
def _deselect_all_devices(self):
"""Uncheck all device checkboxes."""
for i in range(self.table.rowCount()):
item = self.table.item(i, 0)
if item:
item.setCheckState(Qt.CheckState.Unchecked)
def _show_history(self):
"""Show history of flashed devices in this session."""
if not self.flashed_macs:
QMessageBox.information(self, "Flash History", "No devices have been flashed in this session.")
else:
lines = []
for mac in sorted(self.flashed_macs.keys()):
ip, _, result, ts = self.flashed_macs[mac]
icon = "" if result.startswith("DONE") else ""
lines.append(f"[{ts}] {icon} {ip} ({mac}) — {result}")
msg = f"Flash History ({len(self.flashed_macs)} device(s)):\n\n" + "\n".join(lines)
QMessageBox.information(self, "Flash History", msg)
# ── Actions ──
def select_fw(self):
file, _ = QFileDialog.getOpenFileName(
self, "Select Firmware", "",
"Firmware Files (*.bin *.hex *.uf2);;All Files (*)"
)
if file:
self.firmware = file
name = file.split("/")[-1]
self.fw_label.setText(f"{name}")
self.fw_label.setStyleSheet("color: #a6e3a1; font-weight: bold; font-size: 12px; border: none;")
def scan(self):
network_str = self.net_input.text().strip()
try:
network = ipaddress.ip_network(network_str, strict=False)
except ValueError:
QMessageBox.warning(
self, "Invalid Network",
f"'{network_str}' is not a valid network.\n"
"Example: 192.168.4.0/24"
)
return
self.scan_status.setText("⏳ Preparing scan...")
self.scan_status.setStyleSheet("color: #f9e2af;")
self.table.setRowCount(0)
self.devices = []
self.scan_progress_bar.setRange(0, 0)
self.scan_progress_bar.setFormat(" Starting...")
self.scan_progress_bar.setVisible(True)
self.btn_scan.setEnabled(False)
self.scan_thread = ScanThread(network)
self.scan_thread.finished.connect(self._on_scan_done)
self.scan_thread.error.connect(self._on_scan_error)
self.scan_thread.scan_progress.connect(self._on_scan_progress)
self.scan_thread.stage.connect(self._on_scan_stage)
self.scan_thread.start()
def _on_scan_done(self, results):
self.scan_progress_bar.setVisible(False)
self.btn_scan.setEnabled(True)
self.all_devices = []
for dev in results:
dev["status"] = "READY"
self.all_devices.append(dev)
self.all_devices.sort(key=lambda d: ipaddress.ip_address(d["ip"]))
# Apply filter and populate table
self._refresh_table()
total_all = len(self.all_devices)
total_shown = len(self.devices)
self.scan_status.setText(f"✅ Scan complete — {total_all} found, {total_shown} shown")
self.scan_status.setStyleSheet("color: #a6e3a1;")
def _on_scan_stage(self, stage):
if stage == "ping":
self.scan_progress_bar.setFormat("⚡ Pinging hosts... %v / %m")
self.scan_status.setText("⏳ Pinging all hosts...")
self.scan_status.setStyleSheet("color: #f9e2af;")
elif stage == "arp":
self.scan_progress_bar.setRange(0, 0)
self.scan_progress_bar.setFormat(" Reading ARP cache...")
self.scan_status.setText("⏳ Reading ARP cache...")
elif stage == "scapy":
self.scan_progress_bar.setRange(0, 0)
self.scan_progress_bar.setFormat(" ARP broadcast scan...")
self.scan_status.setText("⏳ Running ARP broadcast scan...")
def _on_scan_progress(self, done, total):
self.scan_progress_bar.setRange(0, total)
self.scan_progress_bar.setValue(done)
self.scan_status.setText(f"⏳ Pinging hosts... {done} / {total}")
self.scan_status.setStyleSheet("color: #f9e2af;")
def _on_scan_error(self, error_msg):
self.scan_progress_bar.setVisible(False)
self.btn_scan.setEnabled(True)
self.scan_status.setText("❌ Scan failed")
self.scan_status.setStyleSheet("color: #f38ba8;")
QMessageBox.critical(
self, "Scan Error", f"Failed to scan network:\n{error_msg}"
)
def _on_mode_changed(self, index):
"""Show/hide method based on selected mode."""
mode = self.mode_combo.currentData()
if mode == "update":
self.method_container.setVisible(False)
self.update_warning_lbl.setVisible(True)
else:
self.method_container.setVisible(True)
self.update_warning_lbl.setVisible(False)
self._on_method_changed(self.method_combo.currentIndex())
def _on_method_changed(self, index):
"""SSH credentials are always hidden; credentials are hardcoded."""
self.ssh_creds_widget.setVisible(False)
def _get_selected_devices(self):
"""Return list of (table_row_index, device_dict) for checked devices."""
selected = []
for i in range(self.table.rowCount()):
item = self.table.item(i, 0)
if item and item.checkState() == Qt.CheckState.Checked:
if i < len(self.devices):
selected.append((i, self.devices[i]))
return selected
def flash_all(self):
if not self.firmware:
QMessageBox.warning(
self, "No Firmware",
"Please select a firmware file first."
)
return
if not self.devices:
QMessageBox.information(
self, "No Devices",
"No devices found to flash.\n"
"Please scan the network first."
)
return
selected = self._get_selected_devices()
if not selected:
QMessageBox.information(
self, "No Selection",
"No devices selected.\n"
"Check the boxes next to the devices you want to flash."
)
return
total = len(selected)
self.progress.setMaximum(total)
self.progress.setValue(0)
# Determine flash method and SSH credentials
mode = self.mode_combo.currentData()
# Validate: IP 192.168.11.102 chỉ được nạp ở chế độ Update FW
if mode != "update":
blocked = [dev["ip"] for _, dev in selected if dev["ip"] == "192.168.11.102"]
if blocked:
QMessageBox.warning(
self, "Not Allowed",
"⚠️ Device 192.168.11.102 can only be flashed in Update FW mode.\n"
"Please deselect this device or switch to Update FW mode."
)
return
# Build list with row indices for UI updates
self._flash_row_map = {}
flash_devices = []
for idx, (row, dev) in enumerate(selected):
self._flash_row_map[idx] = row
flash_devices.append(dev)
if mode == "update":
# Hỏi xác nhận nếu có device nào khác 192.168.11.102
strange_ips = [dev["ip"] for _, dev in selected if dev["ip"] != "192.168.11.102"]
if strange_ips:
msg = (f"⚠️ Detected IP(s) other than [192.168.11.102] in your selection:\n"
f"{', '.join(strange_ips[:3])}{'...' if len(strange_ips) > 3 else ''}\n\n"
f"Are you SURE you want to update firmware on these devices?")
reply = QMessageBox.question(
self, "Confirm Update Target", msg,
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.No:
return
method = "ssh"
ssh_user = "root"
ssh_password = "admin123a"
ssh_backup_password = "admin"
set_passwd = False
else:
method = self.method_combo.currentData()
ssh_user = "root"
ssh_password = "admin123a"
ssh_backup_password = "admin123a"
set_passwd = True if method == "ssh" else False
# Chọn đúng worker theo mode và chạy trong background thread
max_w = self.parallel_spin.value()
if mode == "update":
self.flash_thread = UpdateFlashThread(
flash_devices, self.firmware,
max_workers=max_w,
)
else:
self.flash_thread = NewFlashThread(
flash_devices, self.firmware,
max_workers=max_w,
method=method,
ssh_user=ssh_user,
ssh_password=ssh_password,
ssh_backup_password=ssh_backup_password,
set_passwd=set_passwd,
)
self.flash_thread.device_status.connect(self._on_flash_status)
self.flash_thread.device_done.connect(self._on_flash_done)
self.flash_thread.all_done.connect(self._on_flash_all_done)
# Disable flash button trong khi đang flash
self.btn_flash.setEnabled(False)
self.flash_thread.start()
def _on_flash_status(self, index, msg):
"""Update status column while flashing."""
row = self._flash_row_map.get(index, index)
self.table.setItem(row, 3, QTableWidgetItem(f"{msg}"))
def _on_flash_done(self, index, result):
"""One device finished flashing."""
row = self._flash_row_map.get(index, index)
mac_item = self.table.item(row, 2)
ip_item = self.table.item(row, 1)
import datetime
now_str = datetime.datetime.now().strftime("%H:%M:%S")
mac_str = mac_item.text().strip() if mac_item else ""
ip_str = ip_item.text().strip() if ip_item else ""
if result.startswith("DONE"):
item = QTableWidgetItem(f"{result}")
item.setForeground(QColor("#a6e3a1"))
# Auto-uncheck so it won't be flashed again
cb = self.table.item(row, 0)
if cb:
cb.setCheckState(Qt.CheckState.Unchecked)
else:
item = QTableWidgetItem(f"{result}")
item.setForeground(QColor("#f38ba8"))
# Lưu vào lịch sử nạp
if mac_str:
self.flashed_macs[mac_str] = (ip_str, mac_str, result, now_str)
self.table.setItem(row, 3, item)
self.progress.setValue(self.progress.value() + 1)
def _on_flash_all_done(self):
"""All flashing complete."""
self.btn_flash.setEnabled(True)
QMessageBox.information(self, "Flash Complete", "All devices have been processed.")
def _open_auto_flash(self):
"""Switch to Auto Flash UI."""
# Sync firmware & network from main to auto
self.auto_fw_label.setText(
os.path.basename(self.firmware) if self.firmware else "Not selected"
)
self.auto_fw_label.setStyleSheet(
"color: #a6e3a1; font-weight: bold; font-size: 12px;" if self.firmware
else "color: #f38ba8; font-size: 12px;"
)
self.auto_net_input.setText(self.net_input.text().strip())
self.auto_firmware = self.firmware
self.main_container.setVisible(False)
self.auto_container.setVisible(True)
def _back_to_main(self):
"""Return to main view."""
self.auto_container.setVisible(False)
self.main_container.setVisible(True)
# ── Auto Flash UI Builder ──
def _build_auto_ui(self):
"""Build the Auto Firmware Flash UI inside auto_container."""
self.auto_firmware = self.firmware
self._auto_worker = None
self._auto_device_rows = {}
self._auto_log_lines = []
self._auto_success_count = 0
self._auto_fail_count = 0
self._auto_history = []
auto_layout = QVBoxLayout(self.auto_container)
auto_layout.setSpacing(4)
auto_layout.setContentsMargins(10, 8, 10, 8)
# ── Back button + Title row ──
top_row = QHBoxLayout()
top_row.setSpacing(8)
self.btn_back = QPushButton("⬅ Back")
self.btn_back.setFixedHeight(32)
self.btn_back.setStyleSheet("""
QPushButton {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #374151, stop:1 #4b5563);
border-color: #4b5563; color: #ffffff;
font-size: 12px; font-weight: bold;
border-radius: 6px; padding: 4px 16px;
}
QPushButton:hover {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #4b5563, stop:1 #6b7280);
}
""")
self.btn_back.clicked.connect(self._back_to_main)
top_row.addWidget(self.btn_back)
auto_title = QLabel("🤖 Auto Firmware Flash")
auto_title.setStyleSheet("font-size: 16px; font-weight: bold; color: #c4b5fd; letter-spacing: 1px;")
auto_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
top_row.addWidget(auto_title, 1)
auto_layout.addLayout(top_row)
# ── Config group ──
config_group = CollapsibleGroupBox("⚙️ Flash Configuration")
config_layout = QVBoxLayout()
config_layout.setSpacing(6)
config_layout.setContentsMargins(8, 4, 8, 4)
# Row 1: Firmware + Network
row1 = QHBoxLayout()
row1.setSpacing(12)
fw_lbl = QLabel("FW:")
fw_lbl.setStyleSheet("font-weight: bold; font-size: 12px;")
row1.addWidget(fw_lbl)
self.auto_fw_label = QLabel(os.path.basename(self.firmware) if self.firmware else "Not selected")
self.auto_fw_label.setStyleSheet(
"color: #a6e3a1; font-weight: bold; font-size: 12px;" if self.firmware
else "color: #f38ba8; font-size: 12px;"
)
row1.addWidget(self.auto_fw_label)
btn_fw = QPushButton("📁")
btn_fw.setFixedSize(40, 30)
btn_fw.setStyleSheet("font-size: 16px;")
btn_fw.setToolTip("Select firmware")
btn_fw.clicked.connect(self._auto_select_firmware)
row1.addWidget(btn_fw)
sep1 = QLabel("")
sep1.setStyleSheet("color: #3d4a6b; font-size: 14px;")
row1.addWidget(sep1)
net_lbl = QLabel("Network:")
net_lbl.setStyleSheet("font-weight: bold; font-size: 12px;")
row1.addWidget(net_lbl)
self.auto_net_input = QLineEdit(get_default_network(self.local_ip))
self.auto_net_input.setPlaceholderText("e.g. 192.168.4.0/24")
self.auto_net_input.setMaximumWidth(180)
self.auto_net_input.setFixedHeight(28)
self.auto_net_input.setStyleSheet("QLineEdit { font-size: 12px; padding: 3px 8px; }")
row1.addWidget(self.auto_net_input)
row1.addStretch()
config_layout.addLayout(row1)
# Row 2: Target count + Method + Concurrent
row2 = QHBoxLayout()
row2.setSpacing(12)
cnt_lbl = QLabel("Target Count:")
cnt_lbl.setStyleSheet("font-weight: bold; font-size: 13px; color: #e2e8f0;")
row2.addWidget(cnt_lbl)
self.auto_target_spin = QSpinBox()
self.auto_target_spin.setRange(1, 500)
self.auto_target_spin.setValue(5)
self.auto_target_spin.setMinimumWidth(80)
self.auto_target_spin.setFixedHeight(34)
self.auto_target_spin.setStyleSheet(
"QSpinBox { font-size: 14px; font-weight: bold; color: #e2e8f0; "
"background-color: #2d3352; border: 1px solid #3d4a6b; border-radius: 6px; padding: 2px 6px; } "
"QSpinBox::up-button { width: 18px; } QSpinBox::down-button { width: 18px; }"
)
row2.addWidget(self.auto_target_spin)
sep2 = QLabel("")
sep2.setStyleSheet("color: #3d4a6b; font-size: 14px;")
row2.addWidget(sep2)
meth_lbl = QLabel("Method:")
meth_lbl.setStyleSheet("font-weight: bold; font-size: 12px;")
row2.addWidget(meth_lbl)
self.auto_method_combo = QComboBox()
self.auto_method_combo.addItem("🌐 API (LuCI)", "api")
self.auto_method_combo.addItem("💻 SSH", "ssh")
self.auto_method_combo.setFixedHeight(34)
self.auto_method_combo.setMinimumWidth(140)
self.auto_method_combo.setStyleSheet("""
QComboBox {
background-color: #2d3352; border: 1px solid #3d4a6b; border-radius: 6px;
padding: 2px 8px; color: #e2e8f0; font-size: 13px; font-weight: bold;
}
QComboBox:hover { border-color: #7eb8f7; }
QComboBox::drop-down { border: none; width: 20px; }
QComboBox QAbstractItemView {
background-color: #2d3352; color: #e2e8f0;
border: 1px solid #3d4a6b; selection-background-color: #3d4a6b;
}
""")
row2.addWidget(self.auto_method_combo)
sep3 = QLabel("")
sep3.setStyleSheet("color: #3d4a6b; font-size: 14px;")
row2.addWidget(sep3)
par_lbl = QLabel("Concurrent:")
par_lbl.setStyleSheet("font-weight: bold; font-size: 13px; color: #e2e8f0;")
row2.addWidget(par_lbl)
self.auto_parallel_spin = QSpinBox()
self.auto_parallel_spin.setRange(0, 100)
self.auto_parallel_spin.setValue(10)
self.auto_parallel_spin.setSpecialValueText("")
self.auto_parallel_spin.setToolTip("0 = unlimited")
self.auto_parallel_spin.setMinimumWidth(80)
self.auto_parallel_spin.setFixedHeight(34)
self.auto_parallel_spin.setStyleSheet(
"QSpinBox { font-size: 14px; font-weight: bold; color: #e2e8f0; "
"background-color: #2d3352; border: 1px solid #3d4a6b; border-radius: 6px; padding: 2px 6px; } "
"QSpinBox::up-button { width: 18px; } QSpinBox::down-button { width: 18px; }"
)
row2.addWidget(self.auto_parallel_spin)
row2.addStretch()
config_layout.addLayout(row2)
config_group.set_content_layout(config_layout)
auto_layout.addWidget(config_group)
# ── Control Buttons ──
btn_row = QHBoxLayout()
btn_row.setSpacing(8)
self.auto_btn_start = QPushButton("▶ CONFIRM & START")
self.auto_btn_start.setObjectName("start_btn")
self.auto_btn_start.setFixedHeight(36)
self.auto_btn_start.setStyleSheet("""
QPushButton#start_btn {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #7c3aed, stop:1 #a78bfa);
border-color: #7c3aed; color: #ffffff;
font-size: 14px; font-weight: bold; letter-spacing: 1px;
}
QPushButton#start_btn:hover {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #8b5cf6, stop:1 #c4b5fd);
}
QPushButton#start_btn:disabled { background: #3d3d5c; color: #6b7280; }
""")
self.auto_btn_start.clicked.connect(self._auto_on_start)
btn_row.addWidget(self.auto_btn_start)
self.auto_btn_stop = QPushButton("⏹ STOP")
self.auto_btn_stop.setObjectName("stop_btn")
self.auto_btn_stop.setFixedHeight(36)
self.auto_btn_stop.setEnabled(False)
self.auto_btn_stop.setStyleSheet("""
QPushButton#stop_btn {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #dc2626, stop:1 #ef4444);
border-color: #dc2626; color: #ffffff;
font-size: 14px; font-weight: bold;
}
QPushButton#stop_btn:hover {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #ef4444, stop:1 #f87171);
}
QPushButton#stop_btn:disabled { background: #3d3d5c; color: #6b7280; }
""")
self.auto_btn_stop.clicked.connect(self._auto_on_stop)
btn_row.addWidget(self.auto_btn_stop)
auto_layout.addLayout(btn_row)
# ── Status + Progress ──
status_row = QHBoxLayout()
status_row.setSpacing(8)
self.auto_status_label = QLabel("⏸ Waiting to start...")
self.auto_status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #94a3b8;")
status_row.addWidget(self.auto_status_label, 1)
self.auto_progress_bar = QProgressBar()
self.auto_progress_bar.setFormat("%v / %m (%p%)")
self.auto_progress_bar.setFixedHeight(18)
self.auto_progress_bar.setFixedWidth(250)
self.auto_progress_bar.setVisible(False)
status_row.addWidget(self.auto_progress_bar)
auto_layout.addLayout(status_row)
# ── Device Table ──
dev_group = QGroupBox("📋 Device List")
dev_layout = QVBoxLayout()
dev_layout.setSpacing(2)
dev_layout.setContentsMargins(4, 12, 4, 4)
self.auto_result_table = QTableWidget()
self.auto_result_table.setColumnCount(4)
self.auto_result_table.setHorizontalHeaderLabels(["#", "IP", "MAC", "Result"])
self.auto_result_table.setAlternatingRowColors(True)
header = self.auto_result_table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
header.resizeSection(0, 35)
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Interactive)
header.resizeSection(1, 120)
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Interactive)
header.resizeSection(2, 140)
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
self.auto_result_table.verticalHeader().setVisible(False)
self.auto_result_table.setStyleSheet("QTableWidget { font-size: 12px; }")
dev_layout.addWidget(self.auto_result_table)
# Summary row
summary_row = QHBoxLayout()
summary_row.setSpacing(6)
self.auto_summary_label = QLabel("")
self.auto_summary_label.setStyleSheet("color: #94a3b8; font-size: 11px; padding: 2px;")
summary_row.addWidget(self.auto_summary_label, 1)
btn_auto_history = QPushButton("📋 Flash History")
btn_auto_history.setFixedHeight(24)
btn_auto_history.setStyleSheet("background-color: #313244; color: #cdd6f4; font-size: 11px; padding: 2px 8px;")
btn_auto_history.clicked.connect(self._show_auto_history)
summary_row.addWidget(btn_auto_history)
dev_layout.addLayout(summary_row)
dev_group.setLayout(dev_layout)
auto_layout.addWidget(dev_group, stretch=3)
# ── Log ──
log_group = CollapsibleGroupBox("📝 Log")
log_layout = QVBoxLayout()
log_layout.setContentsMargins(4, 2, 4, 2)
self.auto_log_area = QScrollArea()
self.auto_log_content = QLabel("")
self.auto_log_content.setWordWrap(True)
self.auto_log_content.setAlignment(Qt.AlignmentFlag.AlignTop)
self.auto_log_content.setStyleSheet(
"color: #cdd6f4; font-size: 12px; font-family: 'Consolas', 'Courier New', monospace;"
"padding: 6px; background-color: #11121d; border-radius: 4px;"
)
self.auto_log_content.setTextFormat(Qt.TextFormat.PlainText)
self.auto_log_area.setWidget(self.auto_log_content)
self.auto_log_area.setWidgetResizable(True)
self.auto_log_area.setMinimumHeight(150)
self.auto_log_area.setStyleSheet("QScrollArea { border: 1px solid #2d3748; border-radius: 4px; background-color: #11121d; }")
log_layout.addWidget(self.auto_log_area)
log_group.set_content_layout(log_layout)
auto_layout.addWidget(log_group, stretch=1)
# ── Auto Flash Actions ──
def _show_auto_history(self):
if not self._auto_history:
QMessageBox.information(self, "Flash History", "No devices have been flashed in this session.")
return
lines = []
for ip, mac, result, ts in self._auto_history:
icon = "" if result.startswith("DONE") else ""
lines.append(f"[{ts}] {icon} {ip} ({mac}) — {result}")
msg = f"Flash History ({len(self._auto_history)} device(s)):\n\n" + "\n".join(lines)
QMessageBox.information(self, "Flash History", msg)
def _auto_select_firmware(self):
file, _ = QFileDialog.getOpenFileName(
self, "Select Firmware", "",
"Firmware Files (*.bin *.hex *.uf2);;All Files (*)"
)
if file:
self.auto_firmware = file
self.firmware = file # sync back to main
self.auto_fw_label.setText(os.path.basename(file))
self.auto_fw_label.setStyleSheet("color: #a6e3a1; font-weight: bold; font-size: 12px;")
# Also update main UI
name = file.split("/")[-1]
self.fw_label.setText(f"{name}")
self.fw_label.setStyleSheet("color: #a6e3a1; font-weight: bold; font-size: 11px;")
def _auto_append_log(self, msg):
self._auto_log_lines.append(msg)
if len(self._auto_log_lines) > 500:
self._auto_log_lines = self._auto_log_lines[-500:]
self.auto_log_content.setText("\n".join(self._auto_log_lines))
QTimer.singleShot(0, lambda: self.auto_log_area.verticalScrollBar().setValue(
self.auto_log_area.verticalScrollBar().maximum()
))
def _auto_on_start(self):
if not self.auto_firmware:
QMessageBox.warning(self, "No Firmware Selected", "Please select a firmware file first.")
return
network_str = self.auto_net_input.text().strip()
try:
ipaddress.ip_network(network_str, strict=False)
except ValueError:
QMessageBox.warning(self, "Invalid Network", f"'{network_str}' is not a valid network.\nExample: 192.168.4.0/24")
return
target = self.auto_target_spin.value()
method = self.auto_method_combo.currentData()
max_workers = self.auto_parallel_spin.value()
reply = QMessageBox.question(
self, "Confirm",
f"Start auto firmware flash?\n\n"
f" Firmware: {os.path.basename(self.auto_firmware)}\n"
f" Network: {network_str}\n"
f" Target count: {target} device(s)\n"
f" Method: {method.upper()}\n"
f" Concurrent: {max_workers if max_workers > 0 else 'Unlimited'}\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.Yes,
)
if reply != QMessageBox.StandardButton.Yes:
return
# Reset state
self._auto_log_lines = []
self.auto_log_content.setText("")
self.auto_result_table.setRowCount(0)
self._auto_device_rows = {}
self._auto_success_count = 0
self._auto_fail_count = 0
self._auto_history.clear()
self.auto_summary_label.setText("")
self.auto_progress_bar.setVisible(False)
self.auto_btn_start.setEnabled(False)
self.auto_btn_stop.setEnabled(True)
self.auto_net_input.setEnabled(False)
self.auto_target_spin.setEnabled(False)
self.auto_method_combo.setEnabled(False)
self.auto_parallel_spin.setEnabled(False)
self.btn_back.setEnabled(False)
self.auto_status_label.setText("🔍 Scanning LAN...")
self.auto_status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #f9e2af;")
self._auto_worker = AutoFlashWorker(
network=network_str,
target_count=target,
method=method,
max_workers=max_workers,
firmware_path=self.auto_firmware,
local_ip=self.local_ip,
gateway_ip=self.gateway_ip,
)
self._auto_worker.log_message.connect(self._auto_append_log)
self._auto_worker.scan_found.connect(self._auto_on_scan_found)
self._auto_worker.devices_ready.connect(self._auto_on_devices_ready)
self._auto_worker.device_status.connect(self._auto_on_device_status)
self._auto_worker.device_done.connect(self._auto_on_device_done)
self._auto_worker.flash_progress.connect(self._auto_on_flash_progress)
self._auto_worker.all_done.connect(self._auto_on_all_done)
self._auto_worker.scan_timeout.connect(self._auto_on_scan_timeout)
self._auto_worker.stopped.connect(self._auto_on_stopped)
self._auto_worker.start()
def _auto_on_stop(self):
if self._auto_worker:
self._auto_worker.stop()
self.auto_btn_stop.setEnabled(False)
self.auto_status_label.setText("⏳ Stopping...")
def _auto_on_scan_found(self, count):
target = self.auto_target_spin.value()
self.auto_status_label.setText(f"🔍 Scan: found {count}/{target} device(s)...")
if count >= target:
self.auto_status_label.setText(f"{target} device(s) found — flashing firmware...")
self.auto_status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #a6e3a1;")
def _auto_on_devices_ready(self, devices):
self.auto_result_table.setRowCount(0)
self._auto_device_rows = {}
for i, dev in enumerate(devices):
self.auto_result_table.insertRow(i)
num_item = QTableWidgetItem(str(i + 1))
num_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.auto_result_table.setItem(i, 0, num_item)
self.auto_result_table.setItem(i, 1, QTableWidgetItem(dev["ip"]))
self.auto_result_table.setItem(i, 2, QTableWidgetItem(dev.get("mac", "N/A").upper()))
waiting_item = QTableWidgetItem("⏳ Waiting...")
waiting_item.setForeground(QColor("#94a3b8"))
self.auto_result_table.setItem(i, 3, waiting_item)
self._auto_device_rows[dev["ip"]] = i
self.auto_summary_label.setText(f"Total: {len(devices)} device(s)")
def _auto_on_device_status(self, ip, msg):
row = self._auto_device_rows.get(ip)
if row is not None:
item = QTableWidgetItem(f"{msg}")
item.setForeground(QColor("#f9e2af"))
self.auto_result_table.setItem(row, 3, item)
def _auto_on_device_done(self, ip, mac, result):
if ip not in self._auto_device_rows:
row = self.auto_result_table.rowCount()
self.auto_result_table.insertRow(row)
num_item = QTableWidgetItem(str(row + 1))
num_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.auto_result_table.setItem(row, 0, num_item)
self.auto_result_table.setItem(row, 1, QTableWidgetItem(ip))
self.auto_result_table.setItem(row, 2, QTableWidgetItem(mac.upper()))
self._auto_device_rows[ip] = row
row = self._auto_device_rows[ip]
now_str = datetime.datetime.now().strftime("%H:%M:%S")
if result.startswith("DONE"):
item = QTableWidgetItem(f"{result}")
item.setForeground(QColor("#a6e3a1"))
self._auto_success_count += 1
self.flashed_macs[mac.upper()] = (ip, mac.upper(), result, now_str)
else:
item = QTableWidgetItem(f"{result}")
item.setForeground(QColor("#f38ba8"))
self._auto_fail_count += 1
self.auto_result_table.setItem(row, 3, item)
self._auto_history.append((ip, mac.upper(), result, now_str))
total = len(self._auto_device_rows)
done = self._auto_success_count + self._auto_fail_count
self.auto_summary_label.setText(
f"Total: {total} | Done: {done} | ✅ {self._auto_success_count} | ❌ {self._auto_fail_count}"
)
def _auto_on_flash_progress(self, done, total):
self.auto_progress_bar.setVisible(True)
self.auto_progress_bar.setMaximum(total)
self.auto_progress_bar.setValue(done)
self.auto_status_label.setText(f"⚡ Flashing: {done}/{total} device(s)...")
def _auto_on_all_done(self, success, fail):
self._auto_reset_controls()
self.auto_status_label.setText(f"🏁 Complete! ✅ {success} | ❌ {fail}")
self.auto_status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #a6e3a1;")
QMessageBox.information(
self, "Complete",
f"Auto firmware flash complete!\n\n"
f"✅ Success: {success}\n"
f"❌ Failed: {fail}",
)
def _auto_on_stopped(self):
self._auto_reset_controls()
self.auto_status_label.setText("⛔ Stopped by user")
self.auto_status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #f38ba8;")
def _auto_on_scan_timeout(self, found, target):
self._auto_reset_controls()
self.auto_status_label.setText(f"⚠️ Scan timed out: only found {found}/{target} device(s)")
self.auto_status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #fab387;")
QMessageBox.warning(
self, "Not Enough Devices",
f"Scan reached maximum attempts but only found {found}/{target} device(s).\n\n"
f"Please check:\n"
f" • Devices are powered on and connected to the network\n"
f" • Network range ({self.auto_net_input.text()}) is correct\n"
f" • Try again after verifying",
)
def _auto_reset_controls(self):
self.auto_btn_start.setEnabled(True)
self.auto_btn_stop.setEnabled(False)
self.auto_net_input.setEnabled(True)
self.auto_target_spin.setEnabled(True)
self.auto_method_combo.setEnabled(True)
self.auto_parallel_spin.setEnabled(True)
self.btn_back.setEnabled(True)
self._auto_worker = None
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setStyleSheet(STYLE)
window = App()
# 60% height, limited width (750px), centered
screen = app.primaryScreen().availableGeometry()
w = min(750, screen.width())
h = int(screen.height() * 0.6)
x = (screen.width() - w) // 2
y = screen.y() + (screen.height() - h) // 2
window.setGeometry(x, y, w, h)
window.show()
sys.exit(app.exec())