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

148
README.md
View File

@@ -1,9 +1,10 @@
# ⚡ Mira Firmware Loader
Công cụ desktop dùng để **scan, phát hiện và flash firmware hàng loạt** cho các thiết bị OpenWrt trong mạng LAN.
Hỗ trợ nạp **thủ công** (chọn thiết bị → flash) và **tự động hóa** (scan → phát hiện → flash → retry).
> **Tech stack:** Python 3.9+ · PyQt6 · Paramiko/SCP · Scapy · Requests · PyInstaller
> **Phiên bản:** `1.1.3`
> **Phiên bản:** `1.2.0`
---
@@ -11,7 +12,7 @@ Công cụ desktop dùng để **scan, phát hiện và flash firmware hàng lo
```text
Mira_Firmware_Loader/
├── main.py # UI chính (PyQt6)
├── main.py # UI chính (PyQt6) — App + AutoFlashWindow
├── version.txt # Số phiên bản ứng dụng
├── requirements.txt # Danh sách dependencies
├── core/
@@ -22,17 +23,19 @@ Mira_Firmware_Loader/
│ ├── ssh_new_flash.py # Luồng SSH cho chế độ Nạp Mới (Telnet → set passwd → SSH)
│ ├── ssh_update_flash.py # Luồng SSH cho chế độ Update (SSH trực tiếp)
│ ├── flash_new_worker.py # NewFlashThread — QThread điều phối Nạp Mới FW
── flash_update_worker.py # UpdateFlashThread — QThread điều phối Update FW
── flash_update_worker.py # UpdateFlashThread — QThread điều phối Update FW
│ └── auto_flash_worker.py # AutoFlashWorker — QThread tự động scan → flash → retry
├── ui/
│ ├── components.py # Custom Qt Widgets (CollapsibleGroupBox)
│ └── styles.py # Stylesheet toàn ứng dụng
│ └── styles.py # Stylesheet toàn ứng dụng (STYLE + AUTO_STYLE)
├── utils/
│ ├── network.py # Helper IP / network (get_local_ip, get_default_network)
│ └── system.py # Lấy thông tin máy, resource path cho PyInstaller
├── docs/
│ ├── api_flash_docs.md # Tài liệu kỹ thuật LuCI API flash
│ ├── load_fw_ssh_docs.md # Tài liệu kỹ thuật SSH flash (cả 2 luồng)
── scanner_docs.md # Tài liệu kỹ thuật scanner
── scanner_docs.md # Tài liệu kỹ thuật scanner
│ └── auto_flash_docs.md # Tài liệu kỹ thuật tự động hóa nạp FW
├── run.sh # Script khởi chạy (macOS/Linux)
├── run.bat # Script khởi chạy (Windows)
└── build_windows.bat # Script đóng gói thành .exe (Windows, PyInstaller)
@@ -89,31 +92,33 @@ Output: `dist\Mira_Firmware_Loader.exe` — không cần cài Python trên máy
│ Method: [ API (LuCI) | SSH (paramiko) ] │
│ Concurrent devices: [SpinBox] │
│ [ ⚡ FLASH SELECTED DEVICES ] │
│ ───────────────────────────────────────────────────── │
│ [ 🤖 Tự động hóa nạp FW ] │
└────────────────────────┬─────────────────────────────────┘
┌──────────────┼──────────────┐
│ │
┌──────▼──────┐ ┌────────▼────────────────┐
│ scanner.py │ │ Flash Workers
│ │
│ 1. Ping │ │ NewFlashThread
│ Sweep │ ├─ method=api
│ 2. arp -a │ │ └── api_flash.py
│ 3. Scapy │ │ └─ method=ssh │
│ ARP │ │ └── ssh_new_flash
└─────────────┘
UpdateFlashThread
│ └─ ssh_update_flash.py │
└─────────────────────────┘
┌──────────▼──────────┐
│ ssh_utils.py │
│ (Transport Layer) │
│ _create_ssh_client │
│ _upload_firmware │
│ _verify_firmware │
│ _sync_and_sysupgr │
└─────────────────────┘
─────────────────┼─────────────────
┌──────▼──────┐ ┌───────▼──────────┐ ┌───▼──────────────────
│ scanner.py │ Flash Workers │AutoFlashWorker │
│ │ (thủ công) │ │ (tự động hóa)
│ 1. Ping │
│ Sweep │ │ NewFlashThread Phase 1: Scan LAN
│ 2. arp -a │ ├─ api_flash (tối đa 15 lần)
│ 3. Scapy │ │ └─ ssh_new_flash Phase 2: Flash
│ ARP │ (auto-retry x3)
└─────────────┘ UpdateFlash ├─ api_flash
Thread│ └─ ssh_new_flash
│ └─ ssh_update │ └──────────────────────┘
──────────────────┘
┌──────────▼──────────┐
│ ssh_utils.py │
│ (Transport Layer) │
│ _create_ssh_client │
│ _upload_firmware │
│ _verify_firmware │
│ _sync_and_sysupgr │
└─────────────────────┘
```
---
@@ -138,7 +143,7 @@ Kết quả được merge theo IP và sort tăng dần trước khi trả về
- Mặc định ẩn gateway và IP máy tính đang chạy (có thể bật "Show all")
- Thiết bị đã flash trong session được đánh dấu "Already Flashed" và tự bỏ tick
### 3. Flash Firmware
### 3. Flash Firmware (Thủ công)
Có 2 chế độ và 2 method, tổng cộng 3 luồng thực thi khác nhau:
@@ -169,7 +174,73 @@ Luồng `ssh_update_flash.py`, SSH trực tiếp (không qua Telnet):
> ⚠️ Update Mode hiển thị cảnh báo nếu IP thiết bị khác `192.168.11.102` và yêu cầu xác nhận.
### 4. Xử lý song song
### 4. 🤖 Tự động hóa nạp FW (MỚI)
Tính năng nạp FW hoàn toàn tự động — chỉ cần cấu hình và nhấn bắt đầu:
```
Cấu hình → Auto Scan LAN → Phát hiện đủ thiết bị → Auto Flash → Auto Retry → Thông báo
```
#### 4.1. Quy trình
| Phase | Mô tả |
| --------------------- | ------------------------------------------------------------------------- |
| **Phase 1: Scan LAN** | Scan mạng liên tục mỗi 5 giây, tối đa **15 lần**, cho đến khi đủ thiết bị |
| **Phase 2: Flash** | Nạp FW song song qua ThreadPoolExecutor, tự động **retry tối đa 3 lần** |
#### 4.2. Cấu hình
| Tham số | Mô tả | Mặc định |
| --------------- | --------------------------------------------- | ------------------- |
| **Firmware** | File firmware (.bin/.hex/.uf2) | Lấy từ cửa sổ chính |
| **Mạng** | Dải mạng LAN cần scan | Tự suy từ IP host |
| **Số lượng** | Số thiết bị cần nạp | 5 |
| **Phương thức** | API (LuCI) hoặc SSH | API (LuCI) |
| **Song song** | Số thiết bị nạp cùng lúc (0 = không giới hạn) | 10 |
#### 4.3. Auto-Retry nạp FW
Khi một thiết bị nạp thất bại, hệ thống tự động retry:
```
Lần 1 → FAIL: Connection timeout
⚠️ Log cảnh báo, chờ 2 giây...
Lần 2 → FAIL: Upload error
⚠️ Log cảnh báo, chờ 2 giây...
Lần 3 → DONE ✅ (hoặc ❌ báo lỗi sau 3 lần)
```
- Tối đa **3 lần retry** mỗi thiết bị (`MAX_FLASH_RETRIES`)
- Chờ **2 giây** giữa mỗi lần retry để thiết bị ổn định
- Nếu hết 3 lần vẫn fail → đánh dấu ❌, tiếp tục thiết bị tiếp theo
#### 4.4. Scan Timeout
- Nếu scan **15 lần** mà chưa đủ thiết bị → dừng và hiện cảnh báo
- Gợi ý kiểm tra: thiết bị đã bật chưa, dải mạng có đúng không
#### 4.5. Quy tắc bảo vệ IP `192.168.11.102`
| Chế độ | Được nạp 192.168.11.102? | Cơ chế |
| ------------------------ | :----------------------: | ----------------------------------- |
| **New Flash (thủ công)** | ❌ Không | Chặn trước khi flash, hiện cảnh báo |
| **Update FW (thủ công)** | ✅ Có | Cho phép bình thường |
| **Tự động hóa** | ❌ Không | Tự động lọc khỏi kết quả scan |
#### 4.6. Lịch sử nạp
Kết quả nạp được lưu ở 2 cấp:
| Nơi lưu | Phạm vi | Format |
| ------------------------------- | ---------------------- | ------------------------------------ |
| `AutoFlashWindow._auto_history` | Phiên tự động hiện tại | `list[(ip, mac, result, timestamp)]` |
| `App.flashed_macs` | Toàn bộ session app | `dict{MAC: (ip, mac, result, ts)}` |
- Cả thành công ✅ lẫn thất bại ❌ đều được ghi lại
- Nút "📋 Lịch sử nạp" hiển thị cùng format ở cả 2 cửa sổ: `[HH:MM:SS] ✅/❌ IP (MAC) — result`
### 5. Xử lý song song
| Tham số | Mô tả |
| ---------------------- | ------------------------------------------------------------------------ |
@@ -188,6 +259,23 @@ Luồng `ssh_update_flash.py`, SSH trực tiếp (không qua Telnet):
| **SSH Password** | `admin123a` | Hardcoded, không hiển thị trên UI |
| **Concurrent devices** | `10` | Số luồng flash song song |
| **Show all** | Tắt | Ẩn gateway và IP máy host |
| **MAX_FLASH_RETRIES** | `3` | Số lần retry nạp FW (tự động) |
| **MAX_SCAN_ROUNDS** | `15` | Số lần scan tối đa (tự động) |
---
## 🛡️ Xử lý lỗi tổng hợp
| Tình huống | Hành vi |
| -------------------------------- | --------------------------------------------------- |
| Scan exception (network error) | Log lỗi, chờ 3s, scan lại (đếm vào MAX_SCAN_ROUNDS) |
| Scan 15 lần chưa đủ thiết bị | Hiện popup cảnh báo, dừng tự động |
| Flash thất bại lần 12 (tự động) | Log cảnh báo, chờ 2s, retry tự động |
| Flash thất bại sau 3 lần retry | Log lỗi, đánh dấu ❌, tiếp tục device tiếp theo |
| User nhấn DỪNG | Set stop flag, dừng scan/flash an toàn |
| IP 192.168.11.102 + New Flash | Chặn ngay, hiện cảnh báo |
| Chưa chọn firmware | Hiện popup cảnh báo, không cho bắt đầu |
| Mạng không hợp lệ | Hiện popup cảnh báo, không cho bắt đầu |
---

View File

@@ -28,7 +28,7 @@ flowchart TD
S1 --> F1{"Lọc bỏ:\n• Local IP\n• Gateway IP\n• 192.168.11.102"}
F1 --> C1{Đủ số lượng\nthiết bị?}
C1 -->|Có| P2["── Phase 2: Flash ──"]
C1 -->|Chưa đủ| C2{Đã scan\n≥ 20 lần?}
C1 -->|Chưa đủ| C2{Đã scan\n≥ 15 lần?}
C2 -->|Chưa| W1["Chờ 5 giây\n→ scan lại"]
W1 --> S1
C2 -->|Rồi| T1["⚠️ scan_timeout\nThông báo lỗi"]
@@ -54,7 +54,7 @@ flowchart TD
| Hằng số | Giá trị | Mô tả |
| ------------------- | ------- | ------------------------------------------------------ |
| `MAX_FLASH_RETRIES` | 3 | Số lần retry tối đa khi nạp FW thất bại cho 1 thiết bị |
| `MAX_SCAN_ROUNDS` | 20 | Số lần scan LAN tối đa trước khi báo timeout |
| `MAX_SCAN_ROUNDS` | 15 | Số lần scan LAN tối đa trước khi báo timeout |
| Scan interval | 5 giây | Khoảng cách giữa các lần scan (check stop mỗi 0.5s) |
| Retry delay | 2 giây | Thời gian chờ trước khi retry nạp FW |
@@ -101,12 +101,12 @@ AutoFlashWorker(
### 6.1. Phase 1 — Scan Mạng LAN
```
Lần 1/20 → scan_network("192.168.11.0/24")
Lần 1/15 → scan_network("192.168.11.0/24")
→ Lọc bỏ: local_ip, gateway_ip, 192.168.11.102
→ Tìm thấy 3/5 thiết bị → chưa đủ
→ Chờ 5 giây...
Lần 2/20 → scan_network(...)
Lần 2/15 → scan_network(...)
→ Tìm thấy 5/5 thiết bị → ĐỦ!
→ Chuyển sang Phase 2
```
@@ -119,7 +119,7 @@ Lần 2/20 → scan_network(...)
**Timeout:**
- Sau **20 lần scan** mà chưa đủ thiết bị → emit `scan_timeout`
- Sau **15 lần scan** mà chưa đủ thiết bị → emit `scan_timeout`
- UI hiện popup cảnh báo kèm gợi ý kiểm tra:
- Thiết bị đã bật và kết nối mạng chưa
- Dải mạng có đúng không
@@ -176,7 +176,7 @@ Mỗi thiết bị được nạp trong `ThreadPoolExecutor` (chạy song song):
├──────────────────────────────────────────┤
│ 📝 Log (thu gọn được) │
│ 🚀 Bắt đầu chế độ tự động hóa nạp FW...│
│ 🔍 Scan lần 1/20... │
│ 🔍 Scan lần 1/15... │
│ Tìm thấy 5/5 thiết bị │
│ ✅ Đủ 5 thiết bị! Bắt đầu nạp FW... │
│ ⚠️ [192.168.11.105] Lần 1 thất bại... │
@@ -228,7 +228,7 @@ Kết quả nạp được lưu ở **2 nơi** với cùng format:
| Tình huống | Hành vi |
| ------------------------------ | --------------------------------------------------- |
| Scan exception (network error) | Log lỗi, chờ 3s, scan lại (đếm vào MAX_SCAN_ROUNDS) |
| Scan 20 lần chưa đủ thiết bị | Emit `scan_timeout`, hiện popup cảnh báo, dừng |
| Scan 15 lần chưa đủ thiết bị | Emit `scan_timeout`, hiện popup cảnh báo, dừng |
| Flash thất bại lần 1-2 | Log cảnh báo, chờ 2s, retry tự động |
| Flash thất bại sau 3 lần retry | Log lỗi, đánh dấu ❌, tiếp tục device tiếp theo |
| User nhấn DỪNG | Set `_stop_flag`, dừng scan/flash, emit `stopped` |
@@ -255,7 +255,7 @@ Nhấn nút **"🤖 Tự động hóa nạp FW"** ở cuối cửa sổ chính.
Nhấn **"▶ XÁC NHẬN & BẮT ĐẦU"** → xác nhận popup → hệ thống tự động:
1. Scan mạng liên tục cho đến khi đủ thiết bị (tối đa 20 lần)
1. Scan mạng liên tục cho đến khi đủ thiết bị (tối đa 15 lần)
2. Nạp FW song song cho tất cả thiết bị tìm được
3. Tự động retry nếu thiết bị nào bị lỗi (tối đa 3 lần)
4. Hiện thông báo tổng hợp khi hoàn thành

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__":

View File

@@ -1 +1 @@
1.2.0
1.2.1