update UI, version
This commit is contained in:
148
README.md
148
README.md
@@ -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 1–2 (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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
654
main.py
@@ -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__":
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.2.0
|
||||
1.2.1
|
||||
|
||||
Reference in New Issue
Block a user