update UI, version

This commit is contained in:
2026-03-09 19:33:00 +07:00
parent 19febeaf3f
commit 042c50536c
4 changed files with 501 additions and 321 deletions

654
main.py
View File

@@ -34,7 +34,7 @@ from utils.system import resource_path, get_machine_info, get_version
from ui.components import CollapsibleGroupBox
from ui.styles import STYLE, AUTO_STYLE
from ui.styles import STYLE
@@ -63,7 +63,14 @@ class App(QWidget):
self.local_ip = info["ip"]
self.gateway_ip = self._guess_gateway(self.local_ip)
layout = QVBoxLayout()
# ── 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)
@@ -105,47 +112,84 @@ class App(QWidget):
# ── Firmware + Network Scan (combined compact row) ──
fw_scan_group = CollapsibleGroupBox("📦 FW & 📡 Scan")
fw_scan_layout = QVBoxLayout()
fw_scan_layout.setSpacing(4)
fw_scan_layout.setContentsMargins(0, 0, 0, 0)
fw_scan_layout.setSpacing(6)
fw_scan_layout.setContentsMargins(4, 2, 4, 4)
# Row 1: FW selection + Scan
top_row = QHBoxLayout()
top_row.setSpacing(8)
# 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_lbl = QLabel("FW:")
fw_lbl.setStyleSheet("font-weight: bold; font-size: 12px;")
top_row.addWidget(fw_lbl)
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.setObjectName("info")
self.fw_label.setStyleSheet("font-size: 11px; color: #94a3b8;")
top_row.addWidget(self.fw_label)
btn_fw = QPushButton("📁")
btn_fw.setFixedSize(32, 26)
btn_fw.setToolTip("Select firmware file")
self.fw_label.setStyleSheet("font-size: 12px; color: #94a3b8; border: none;")
fw_card_layout.addWidget(self.fw_label, 1)
btn_fw = QPushButton("📁 Chọn 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)
top_row.addWidget(btn_fw)
fw_card_layout.addWidget(btn_fw)
fw_scan_layout.addWidget(fw_card)
sep = QLabel("")
sep.setStyleSheet("color: #3d4a6b; font-size: 14px;")
top_row.addWidget(sep)
# 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;")
top_row.addWidget(net_lbl)
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.setMaximumWidth(170)
self.net_input.setFixedHeight(26)
self.net_input.setStyleSheet("QLineEdit { font-size: 12px; padding: 2px 8px; }")
top_row.addWidget(self.net_input)
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(26)
self.btn_scan.setFixedWidth(100)
top_row.addWidget(self.btn_scan)
fw_scan_layout.addLayout(top_row)
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()
@@ -379,10 +423,15 @@ class App(QWidget):
self.btn_auto.clicked.connect(self._open_auto_flash)
layout.addWidget(self.btn_auto)
self.setLayout(layout)
root_layout.addWidget(self.main_container)
# ── AUTO TAB (hidden by default) ──
self.auto_widget = None
# ── 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 ──
@@ -502,7 +551,7 @@ class App(QWidget):
self.firmware = file
name = file.split("/")[-1]
self.fw_label.setText(f"{name}")
self.fw_label.setStyleSheet("color: #a6e3a1; font-weight: bold; font-size: 11px;")
self.fw_label.setStyleSheet("color: #a6e3a1; font-weight: bold; font-size: 12px; border: none;")
def scan(self):
network_str = self.net_input.text().strip()
@@ -749,49 +798,70 @@ class App(QWidget):
QMessageBox.information(self, "Flash Complete", "All devices have been processed.")
def _open_auto_flash(self):
"""Mở cửa sổ Tự động hóa nạp FW."""
if self.auto_widget is None or not self.auto_widget.isVisible():
self.auto_widget = AutoFlashWindow(
firmware=self.firmware,
network=self.net_input.text().strip(),
local_ip=self.local_ip,
gateway_ip=self.gateway_ip,
parent_app=self,
)
self.auto_widget.resize(700, 750)
self.auto_widget.show()
else:
self.auto_widget.raise_()
self.auto_widget.activateWindow()
"""Chuyển sang giao diện Tự động hóa nạp FW."""
# Sync firmware & network từ main sang auto
self.auto_fw_label.setText(
os.path.basename(self.firmware) if self.firmware else "Chưa chọn"
)
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)
class AutoFlashWindow(QWidget):
"""Cửa sổ Tự động hóa nạp FW — scan + flash tự động."""
def _back_to_main(self):
"""Quay lại giao diện chính."""
self.auto_container.setVisible(False)
self.main_container.setVisible(True)
def __init__(self, firmware=None, network="", local_ip="", gateway_ip="", parent_app=None):
super().__init__()
self.setWindowTitle("🤖 Tự động hóa nạp FW")
self.setWindowIcon(QIcon(resource_path("icon.ico")))
self.firmware = firmware
self.local_ip = local_ip
self.gateway_ip = gateway_ip
self.parent_app = parent_app
self.worker = None
self._device_rows = {} # ip -> row index in result table
# ── Auto Flash UI Builder ──
self.setStyleSheet(AUTO_STYLE)
def _build_auto_ui(self):
"""Xây dựng giao diện tự động nạp FW bên trong 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 = []
layout = QVBoxLayout()
layout.setSpacing(4)
layout.setContentsMargins(10, 8, 10, 8)
auto_layout = QVBoxLayout(self.auto_container)
auto_layout.setSpacing(4)
auto_layout.setContentsMargins(10, 8, 10, 8)
# ── Title row (compact) ──
title = QLabel("🤖 Tự động hóa nạp FW")
title.setObjectName("title")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title)
# ── Back button + Title row ──
top_row = QHBoxLayout()
top_row.setSpacing(8)
self.btn_back = QPushButton("⬅ Quay lại")
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)
# ── Top bar: FW + Network + Config (compact, 2 rows) ──
auto_title = QLabel("🤖 Tự động hóa nạp FW")
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("⚙️ Cấu hình nạp")
config_layout = QVBoxLayout()
config_layout.setSpacing(6)
@@ -803,16 +873,17 @@ class AutoFlashWindow(QWidget):
fw_lbl = QLabel("FW:")
fw_lbl.setStyleSheet("font-weight: bold; font-size: 12px;")
row1.addWidget(fw_lbl)
self.fw_label = QLabel(os.path.basename(firmware) if firmware else "Chưa chọn")
self.fw_label.setStyleSheet(
"color: #a6e3a1; font-weight: bold; font-size: 12px;" if firmware
self.auto_fw_label = QLabel(os.path.basename(self.firmware) if self.firmware else "Chưa chọn")
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.fw_label)
row1.addWidget(self.auto_fw_label)
btn_fw = QPushButton("📁")
btn_fw.setFixedSize(32, 28)
btn_fw.setFixedSize(40, 30)
btn_fw.setStyleSheet("font-size: 16px;")
btn_fw.setToolTip("Chọn firmware")
btn_fw.clicked.connect(self._select_firmware)
btn_fw.clicked.connect(self._auto_select_firmware)
row1.addWidget(btn_fw)
sep1 = QLabel("")
@@ -822,12 +893,12 @@ class AutoFlashWindow(QWidget):
net_lbl = QLabel("Mạng:")
net_lbl.setStyleSheet("font-weight: bold; font-size: 12px;")
row1.addWidget(net_lbl)
self.net_input = QLineEdit(network or get_default_network(self.local_ip))
self.net_input.setPlaceholderText("e.g. 192.168.4.0/24")
self.net_input.setMaximumWidth(180)
self.net_input.setFixedHeight(28)
self.net_input.setStyleSheet("QLineEdit { font-size: 12px; padding: 3px 8px; }")
row1.addWidget(self.net_input)
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)
@@ -838,13 +909,13 @@ class AutoFlashWindow(QWidget):
cnt_lbl = QLabel("Số lượng:")
cnt_lbl.setStyleSheet("font-weight: bold; font-size: 12px;")
row2.addWidget(cnt_lbl)
self.target_spin = QSpinBox()
self.target_spin.setRange(1, 500)
self.target_spin.setValue(5)
self.target_spin.setFixedWidth(70)
self.target_spin.setFixedHeight(28)
self.target_spin.setStyleSheet("QSpinBox { font-size: 13px; font-weight: bold; }")
row2.addWidget(self.target_spin)
self.auto_target_spin = QSpinBox()
self.auto_target_spin.setRange(1, 500)
self.auto_target_spin.setValue(5)
self.auto_target_spin.setFixedWidth(70)
self.auto_target_spin.setFixedHeight(28)
self.auto_target_spin.setStyleSheet("QSpinBox { font-size: 13px; font-weight: bold; }")
row2.addWidget(self.auto_target_spin)
sep2 = QLabel("")
sep2.setStyleSheet("color: #3d4a6b; font-size: 14px;")
@@ -853,12 +924,12 @@ class AutoFlashWindow(QWidget):
meth_lbl = QLabel("Phương thức:")
meth_lbl.setStyleSheet("font-weight: bold; font-size: 12px;")
row2.addWidget(meth_lbl)
self.method_combo = QComboBox()
self.method_combo.addItem("🌐 API (LuCI)", "api")
self.method_combo.addItem("💻 SSH", "ssh")
self.method_combo.setFixedHeight(28)
self.method_combo.setMinimumWidth(140)
row2.addWidget(self.method_combo)
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(28)
self.auto_method_combo.setMinimumWidth(140)
row2.addWidget(self.auto_method_combo)
sep3 = QLabel("")
sep3.setStyleSheet("color: #3d4a6b; font-size: 14px;")
@@ -867,64 +938,90 @@ class AutoFlashWindow(QWidget):
par_lbl = QLabel("Song song:")
par_lbl.setStyleSheet("font-weight: bold; font-size: 12px;")
row2.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 = không giới hạn")
self.parallel_spin.setFixedWidth(65)
self.parallel_spin.setFixedHeight(28)
self.parallel_spin.setStyleSheet("QSpinBox { font-size: 13px; font-weight: bold; }")
row2.addWidget(self.parallel_spin)
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 = không giới hạn")
self.auto_parallel_spin.setFixedWidth(65)
self.auto_parallel_spin.setFixedHeight(28)
self.auto_parallel_spin.setStyleSheet("QSpinBox { font-size: 13px; font-weight: bold; }")
row2.addWidget(self.auto_parallel_spin)
row2.addStretch()
config_layout.addLayout(row2)
config_group.set_content_layout(config_layout)
layout.addWidget(config_group)
auto_layout.addWidget(config_group)
# ── Control Buttons (compact) ──
# ── Control Buttons ──
btn_row = QHBoxLayout()
btn_row.setSpacing(8)
self.btn_start = QPushButton("▶ XÁC NHẬN & BẮT ĐẦU")
self.btn_start.setObjectName("start_btn")
self.btn_start.setFixedHeight(36)
self.btn_start.clicked.connect(self._on_start)
btn_row.addWidget(self.btn_start)
self.auto_btn_start = QPushButton("▶ XÁC NHẬN & BẮT ĐẦU")
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.btn_stop = QPushButton("⏹ DỪNG")
self.btn_stop.setObjectName("stop_btn")
self.btn_stop.setFixedHeight(36)
self.btn_stop.setEnabled(False)
self.btn_stop.clicked.connect(self._on_stop)
btn_row.addWidget(self.btn_stop)
layout.addLayout(btn_row)
self.auto_btn_stop = QPushButton("⏹ DỪNG")
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 (inline) ──
# ── Status + Progress ──
status_row = QHBoxLayout()
status_row.setSpacing(8)
self.status_label = QLabel("⏸ Chờ bắt đầu...")
self.status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #94a3b8;")
status_row.addWidget(self.status_label, 1)
self.auto_status_label = QLabel("⏸ Chờ bắt đầu...")
self.auto_status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #94a3b8;")
status_row.addWidget(self.auto_status_label, 1)
self.progress_bar = QProgressBar()
self.progress_bar.setFormat("%v / %m (%p%)")
self.progress_bar.setFixedHeight(18)
self.progress_bar.setFixedWidth(250)
self.progress_bar.setVisible(False)
status_row.addWidget(self.progress_bar)
layout.addLayout(status_row)
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 (MAIN area — stretch) ──
# ── Device Table ──
dev_group = QGroupBox("📋 Danh sách thiết bị")
dev_layout = QVBoxLayout()
dev_layout.setSpacing(2)
dev_layout.setContentsMargins(4, 12, 4, 4)
self.result_table = QTableWidget()
self.result_table.setColumnCount(4)
self.result_table.setHorizontalHeaderLabels(["#", "IP", "MAC", "Kết quả"])
self.result_table.setAlternatingRowColors(True)
header = self.result_table.horizontalHeader()
self.auto_result_table = QTableWidget()
self.auto_result_table.setColumnCount(4)
self.auto_result_table.setHorizontalHeaderLabels(["#", "IP", "MAC", "Kết quả"])
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)
@@ -932,16 +1029,16 @@ class AutoFlashWindow(QWidget):
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Interactive)
header.resizeSection(2, 140)
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
self.result_table.verticalHeader().setVisible(False)
self.result_table.setStyleSheet("QTableWidget { font-size: 12px; }")
dev_layout.addWidget(self.result_table)
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 below table
# Summary row
summary_row = QHBoxLayout()
summary_row.setSpacing(6)
self.summary_label = QLabel("")
self.summary_label.setStyleSheet("color: #94a3b8; font-size: 11px; padding: 2px;")
summary_row.addWidget(self.summary_label, 1)
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("📋 Lịch sử nạp")
btn_auto_history.setFixedHeight(24)
@@ -951,40 +1048,35 @@ class AutoFlashWindow(QWidget):
dev_layout.addLayout(summary_row)
dev_group.setLayout(dev_layout)
layout.addWidget(dev_group, stretch=3)
auto_layout.addWidget(dev_group, stretch=3)
# ── Log (collapsible, compact) ──
# ── Log ──
log_group = CollapsibleGroupBox("📝 Log")
log_layout = QVBoxLayout()
log_layout.setContentsMargins(4, 2, 4, 2)
self.log_area = QScrollArea()
self.log_content = QLabel("")
self.log_content.setWordWrap(True)
self.log_content.setAlignment(Qt.AlignmentFlag.AlignTop)
self.log_content.setStyleSheet(
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: 10px; font-family: 'SF Mono', 'Menlo', monospace;"
"padding: 4px; background-color: #11121d; border-radius: 4px;"
)
self.log_content.setTextFormat(Qt.TextFormat.PlainText)
self.log_area.setWidget(self.log_content)
self.log_area.setWidgetResizable(True)
self.log_area.setMinimumHeight(120)
self.log_area.setMaximumHeight(280)
self.log_area.setStyleSheet("QScrollArea { border: 1px solid #2d3748; border-radius: 4px; background-color: #11121d; }")
log_layout.addWidget(self.log_area)
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(120)
self.auto_log_area.setMaximumHeight(280)
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)
layout.addWidget(log_group, stretch=1)
auto_layout.addWidget(log_group, stretch=1)
self.setLayout(layout)
self._log_lines = []
self._success_count = 0
self._fail_count = 0
self._auto_history = [] # list of (ip, mac, result, timestamp)
# ── Auto Flash Actions ──
def _show_auto_history(self):
"""Hiển thị lịch sử thiết bị đã nạp trong phiên tự động hóa."""
if not self._auto_history:
QMessageBox.information(self, "Lịch sử nạp", "Chưa có thiết bị nào được nạp trong phiên này.")
return
@@ -995,47 +1087,49 @@ class AutoFlashWindow(QWidget):
msg = f"Lịch sử nạp FW ({len(self._auto_history)} thiết bị):\n\n" + "\n".join(lines)
QMessageBox.information(self, "Lịch sử nạp", msg)
def _select_firmware(self):
def _auto_select_firmware(self):
file, _ = QFileDialog.getOpenFileName(
self, "Chọn Firmware", "",
"Firmware Files (*.bin *.hex *.uf2);;All Files (*)"
)
if file:
self.firmware = file
self.fw_label.setText(os.path.basename(file))
self.fw_label.setStyleSheet("color: #a6e3a1; font-weight: bold;")
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 _append_log(self, msg):
self._log_lines.append(msg)
# Giới hạn 500 dòng log
if len(self._log_lines) > 500:
self._log_lines = self._log_lines[-500:]
self.log_content.setText("\n".join(self._log_lines))
# Scroll to bottom
sb = self.log_area.verticalScrollBar()
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))
sb = self.auto_log_area.verticalScrollBar()
sb.setValue(sb.maximum())
def _on_start(self):
if not self.firmware:
def _auto_on_start(self):
if not self.auto_firmware:
QMessageBox.warning(self, "Chưa chọn FW", "Vui lòng chọn file firmware trước.")
return
network_str = self.net_input.text().strip()
network_str = self.auto_net_input.text().strip()
try:
ipaddress.ip_network(network_str, strict=False)
except ValueError:
QMessageBox.warning(self, "Mạng không hợp lệ", f"'{network_str}' không phải network hợp lệ.\nVí dụ: 192.168.4.0/24")
return
target = self.target_spin.value()
method = self.method_combo.currentData()
max_workers = self.parallel_spin.value()
target = self.auto_target_spin.value()
method = self.auto_method_combo.currentData()
max_workers = self.auto_parallel_spin.value()
# Confirm
reply = QMessageBox.question(
self, "Xác nhận",
f"Bắt đầu tự động nạp FW?\n\n"
f" Firmware: {os.path.basename(self.firmware)}\n"
f" Firmware: {os.path.basename(self.auto_firmware)}\n"
f" Mạng: {network_str}\n"
f" Số lượng: {target} thiết bị\n"
f" Phương thức: {method.upper()}\n"
@@ -1047,126 +1141,123 @@ class AutoFlashWindow(QWidget):
return
# Reset state
self._log_lines = []
self.log_content.setText("")
self.result_table.setRowCount(0)
self._device_rows = {}
self._success_count = 0
self._fail_count = 0
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.summary_label.setText("")
self.progress_bar.setVisible(False)
self.auto_summary_label.setText("")
self.auto_progress_bar.setVisible(False)
self.btn_start.setEnabled(False)
self.btn_stop.setEnabled(True)
self.net_input.setEnabled(False)
self.target_spin.setEnabled(False)
self.method_combo.setEnabled(False)
self.parallel_spin.setEnabled(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.status_label.setText("🔍 Đang scan mạng LAN...")
self.status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #f9e2af;")
self.auto_status_label.setText("🔍 Đang scan mạng LAN...")
self.auto_status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #f9e2af;")
self.worker = AutoFlashWorker(
self._auto_worker = AutoFlashWorker(
network=network_str,
target_count=target,
method=method,
max_workers=max_workers,
firmware_path=self.firmware,
firmware_path=self.auto_firmware,
local_ip=self.local_ip,
gateway_ip=self.gateway_ip,
)
self.worker.log_message.connect(self._append_log)
self.worker.scan_found.connect(self._on_scan_found)
self.worker.devices_ready.connect(self._on_devices_ready)
self.worker.device_status.connect(self._on_device_status)
self.worker.device_done.connect(self._on_device_done)
self.worker.flash_progress.connect(self._on_flash_progress)
self.worker.all_done.connect(self._on_all_done)
self.worker.scan_timeout.connect(self._on_scan_timeout)
self.worker.stopped.connect(self._on_stopped)
self.worker.start()
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 _on_stop(self):
if self.worker:
self.worker.stop()
self.btn_stop.setEnabled(False)
self.status_label.setText("⏳ Đang dừng...")
def _auto_on_stop(self):
if self._auto_worker:
self._auto_worker.stop()
self.auto_btn_stop.setEnabled(False)
self.auto_status_label.setText("⏳ Đang dừng...")
def _on_scan_found(self, count):
target = self.target_spin.value()
self.status_label.setText(f"🔍 Scan: tìm thấy {count}/{target} thiết bị...")
def _auto_on_scan_found(self, count):
target = self.auto_target_spin.value()
self.auto_status_label.setText(f"🔍 Scan: tìm thấy {count}/{target} thiết bị...")
if count >= target:
self.status_label.setText(f"⚡ Đủ {target} thiết bị — đang nạp FW...")
self.status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #a6e3a1;")
self.auto_status_label.setText(f"⚡ Đủ {target} thiết bị — đang nạp FW...")
self.auto_status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #a6e3a1;")
def _on_devices_ready(self, devices):
"""Pre-populate bảng kết quả trước khi bắt đầu flash."""
self.result_table.setRowCount(0)
self._device_rows = {}
def _auto_on_devices_ready(self, devices):
self.auto_result_table.setRowCount(0)
self._auto_device_rows = {}
for i, dev in enumerate(devices):
self.result_table.insertRow(i)
self.auto_result_table.insertRow(i)
num_item = QTableWidgetItem(str(i + 1))
num_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.result_table.setItem(i, 0, num_item)
self.result_table.setItem(i, 1, QTableWidgetItem(dev["ip"]))
self.result_table.setItem(i, 2, QTableWidgetItem(dev.get("mac", "N/A").upper()))
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("⏳ Đang chờ...")
waiting_item.setForeground(QColor("#94a3b8"))
self.result_table.setItem(i, 3, waiting_item)
self._device_rows[dev["ip"]] = i
self.summary_label.setText(f"Tổng: {len(devices)} thiết bị")
self.auto_result_table.setItem(i, 3, waiting_item)
self._auto_device_rows[dev["ip"]] = i
self.auto_summary_label.setText(f"Tổng: {len(devices)} thiết bị")
def _on_device_status(self, ip, msg):
row = self._device_rows.get(ip)
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.result_table.setItem(row, 3, item)
self.auto_result_table.setItem(row, 3, item)
def _on_device_done(self, ip, mac, result):
# Thêm dòng mới vào bảng kết quả nếu chưa có
if ip not in self._device_rows:
row = self.result_table.rowCount()
self.result_table.insertRow(row)
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.result_table.setItem(row, 0, num_item)
self.result_table.setItem(row, 1, QTableWidgetItem(ip))
self.result_table.setItem(row, 2, QTableWidgetItem(mac.upper()))
self._device_rows[ip] = row
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._device_rows[ip]
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._success_count += 1
# Lưu vào FlashHistory của cửa sổ chính
if self.parent_app:
self.parent_app.flashed_macs[mac.upper()] = (ip, mac.upper(), result, now_str)
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._fail_count += 1
self.result_table.setItem(row, 3, item)
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._device_rows)
done = self._success_count + self._fail_count
self.summary_label.setText(
f"Tổng: {total} | Xong: {done} | ✅ {self._success_count} | ❌ {self._fail_count}"
total = len(self._auto_device_rows)
done = self._auto_success_count + self._auto_fail_count
self.auto_summary_label.setText(
f"Tổng: {total} | Xong: {done} | ✅ {self._auto_success_count} | ❌ {self._auto_fail_count}"
)
def _on_flash_progress(self, done, total):
self.progress_bar.setVisible(True)
self.progress_bar.setMaximum(total)
self.progress_bar.setValue(done)
self.status_label.setText(f"⚡ Nạp FW: {done}/{total} thiết bị...")
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"⚡ Nạp FW: {done}/{total} thiết bị...")
def _on_all_done(self, success, fail):
self._reset_controls()
self.status_label.setText(f"🏁 Hoàn thành! ✅ {success} | ❌ {fail}")
self.status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #a6e3a1;")
def _auto_on_all_done(self, success, fail):
self._auto_reset_controls()
self.auto_status_label.setText(f"🏁 Hoàn thành! ✅ {success} | ❌ {fail}")
self.auto_status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #a6e3a1;")
QMessageBox.information(
self, "Hoàn thành",
f"Tự động nạp FW hoàn thành!\n\n"
@@ -1174,32 +1265,33 @@ class AutoFlashWindow(QWidget):
f"❌ Thất bại: {fail}",
)
def _on_stopped(self):
self._reset_controls()
self.status_label.setText("⛔ Đã dừng bởi người dùng")
self.status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #f38ba8;")
def _auto_on_stopped(self):
self._auto_reset_controls()
self.auto_status_label.setText("⛔ Đã dừng bởi người dùng")
self.auto_status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #f38ba8;")
def _on_scan_timeout(self, found, target):
self._reset_controls()
self.status_label.setText(f"⚠️ Scan hết lần: chỉ tìm thấy {found}/{target} thiết bị")
self.status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #fab387;")
def _auto_on_scan_timeout(self, found, target):
self._auto_reset_controls()
self.auto_status_label.setText(f"⚠️ Scan hết lần: chỉ tìm thấy {found}/{target} thiết bị")
self.auto_status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #fab387;")
QMessageBox.warning(
self, "Không đủ thiết bị",
f"Đã scan tối đa nhưng chỉ tìm thấy {found}/{target} thiết bị.\n\n"
f"Vui lòng kiểm tra:\n"
f" • Thiết bị đã bật và kết nối mạng chưa\n"
f" • Dải mạng ({self.net_input.text()}) có đúng không\n"
f" • Dải mạng ({self.auto_net_input.text()}) có đúng không\n"
f" • Thử lại sau khi kiểm tra",
)
def _reset_controls(self):
self.btn_start.setEnabled(True)
self.btn_stop.setEnabled(False)
self.net_input.setEnabled(True)
self.target_spin.setEnabled(True)
self.method_combo.setEnabled(True)
self.parallel_spin.setEnabled(True)
self.worker = None
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__":