Compare commits
4 Commits
042c50536c
...
V1.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
| f5ee209726 | |||
| 466dadf1c9 | |||
| b4745214f2 | |||
| 910307967b |
@@ -18,7 +18,7 @@ from core.api_flash import flash_device_api
|
||||
from core.ssh_new_flash import flash_device_new_ssh
|
||||
|
||||
MAX_FLASH_RETRIES = 3 # Số lần retry nạp FW khi thất bại
|
||||
MAX_SCAN_ROUNDS = 15 # Số lần scan tối đa trước khi báo không đủ thiết bị
|
||||
MAX_SCAN_ROUNDS = 10 # Số lần scan tối đa trước khi báo không đủ thiết bị
|
||||
|
||||
|
||||
class AutoFlashWorker(QThread):
|
||||
|
||||
124
core/scanner.py
124
core/scanner.py
@@ -1,7 +1,5 @@
|
||||
import subprocess
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import ipaddress
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
@@ -10,146 +8,72 @@ _NO_WINDOW = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
|
||||
|
||||
|
||||
def _ping_one(ip, is_win):
|
||||
"""Ping a single IP to populate ARP table."""
|
||||
"""Ping a single IP. Returns True if host responds."""
|
||||
try:
|
||||
if is_win:
|
||||
subprocess.run(
|
||||
["ping", "-n", "1", "-w", "300", str(ip)], # 300ms (was 600ms)
|
||||
r = subprocess.run(
|
||||
["ping", "-n", "1", "-w", "300", str(ip)],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=2,
|
||||
creationflags=_NO_WINDOW
|
||||
)
|
||||
elif sys.platform == "darwin":
|
||||
subprocess.run(
|
||||
["ping", "-c", "1", "-W", "500", str(ip)], # 500ms — macOS: -W unit là ms
|
||||
r = subprocess.run(
|
||||
["ping", "-c", "1", "-W", "300", str(ip)],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=2
|
||||
)
|
||||
else:
|
||||
subprocess.run(
|
||||
["ping", "-c", "1", "-W", "1", str(ip)], # 1s — Linux: -W unit là giây
|
||||
r = subprocess.run(
|
||||
["ping", "-c", "1", "-W", "1", str(ip)],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=2
|
||||
)
|
||||
return r.returncode == 0
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def _ping_sweep(network, progress_cb=None):
|
||||
"""Ping tất cả host trong network đồng thời để điền ARP cache.
|
||||
Gọi progress_cb(done, total) sau mỗi ping nếu được cung cấp.
|
||||
"""Ping tất cả host trong network đồng thời.
|
||||
Trả về list IP đã phản hồi. Gọi progress_cb(done, total) sau mỗi ping.
|
||||
"""
|
||||
net = ipaddress.ip_network(network, strict=False)
|
||||
|
||||
# Chỉ ping sweep cho /24 hoặc nhỏ hơn
|
||||
if net.num_addresses > 256:
|
||||
return
|
||||
return []
|
||||
|
||||
is_win = sys.platform == "win32"
|
||||
hosts = list(net.hosts())
|
||||
total = len(hosts)
|
||||
done_count = [0]
|
||||
alive = []
|
||||
|
||||
def _ping_and_track(ip):
|
||||
_ping_one(ip, is_win)
|
||||
ok = _ping_one(ip, is_win)
|
||||
done_count[0] += 1
|
||||
if progress_cb:
|
||||
progress_cb(done_count[0], total)
|
||||
return (str(ip), ok)
|
||||
|
||||
# Tất cả host cùng lúc — loại bỏ overhead batching (was max_workers=100)
|
||||
with ThreadPoolExecutor(max_workers=len(hosts)) as executor:
|
||||
futures = [executor.submit(_ping_and_track, ip) for ip in hosts]
|
||||
for f in as_completed(futures):
|
||||
pass
|
||||
ip_str, ok = f.result()
|
||||
if ok:
|
||||
alive.append(ip_str)
|
||||
|
||||
return alive
|
||||
|
||||
|
||||
def scan_network(network, progress_cb=None, stage_cb=None):
|
||||
"""Scan network: ping sweep → ARP table + Scapy song song."""
|
||||
# Phase 1: Ping sweep
|
||||
"""Scan network: chỉ dùng ping để xác định thiết bị online."""
|
||||
if stage_cb:
|
||||
stage_cb("ping")
|
||||
_ping_sweep(network, progress_cb)
|
||||
time.sleep(0.3) # Giảm từ 1s xuống 0.3s
|
||||
alive_ips = _ping_sweep(network, progress_cb)
|
||||
|
||||
# Phase 2 + 3: ARP table và Scapy chạy song song
|
||||
if stage_cb:
|
||||
stage_cb("arp")
|
||||
|
||||
def _collect_arp():
|
||||
result = {}
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
["arp", "-a"], text=True, creationflags=_NO_WINDOW
|
||||
)
|
||||
net = ipaddress.ip_network(network, strict=False)
|
||||
if sys.platform == "win32":
|
||||
pattern = re.compile(
|
||||
r"(\d+\.\d+\.\d+\.\d+)\s+"
|
||||
r"([0-9a-fA-F]{2}-[0-9a-fA-F]{2}-[0-9a-fA-F]{2}-"
|
||||
r"[0-9a-fA-F]{2}-[0-9a-fA-F]{2}-[0-9a-fA-F]{2})\s+"
|
||||
r"(dynamic|static)",
|
||||
re.IGNORECASE
|
||||
)
|
||||
for line in output.splitlines():
|
||||
m = pattern.search(line)
|
||||
if m:
|
||||
ip_str = m.group(1)
|
||||
mac = m.group(2).replace("-", ":")
|
||||
if mac.upper() != "FF:FF:FF:FF:FF:FF":
|
||||
try:
|
||||
if ipaddress.ip_address(ip_str) in net:
|
||||
result[ip_str] = {"ip": ip_str, "mac": mac}
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
pattern = re.compile(
|
||||
r"\((\d+\.\d+\.\d+\.\d+)\)\s+at\s+([0-9a-fA-F:]+)"
|
||||
)
|
||||
for line in output.splitlines():
|
||||
m = pattern.search(line)
|
||||
if m:
|
||||
ip_str, mac = m.group(1), m.group(2)
|
||||
if mac.lower() not in ("(incomplete)", "ff:ff:ff:ff:ff:ff"):
|
||||
try:
|
||||
if ipaddress.ip_address(ip_str) in net:
|
||||
result[ip_str] = {"ip": ip_str, "mac": mac}
|
||||
except ValueError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
def _collect_scapy():
|
||||
result = {}
|
||||
try:
|
||||
import io
|
||||
_stderr = sys.stderr
|
||||
sys.stderr = io.StringIO()
|
||||
try:
|
||||
from scapy.all import ARP, Ether, srp
|
||||
finally:
|
||||
sys.stderr = _stderr
|
||||
|
||||
arp = ARP(pdst=str(network))
|
||||
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
|
||||
raw = srp(ether / arp, timeout=1, verbose=0)[0] # Giảm từ 2s xuống 1s
|
||||
for _, received in raw:
|
||||
result[received.psrc] = {"ip": received.psrc, "mac": received.hwsrc}
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
# Chạy ARP read và Scapy đồng thời
|
||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||
f_arp = executor.submit(_collect_arp)
|
||||
f_scapy = executor.submit(_collect_scapy)
|
||||
seen = f_arp.result()
|
||||
# Scapy bổ sung những IP chưa có trong ARP table
|
||||
for ip, dev in f_scapy.result().items():
|
||||
if ip not in seen:
|
||||
seen[ip] = dev
|
||||
|
||||
return sorted(seen.values(), key=lambda d: ipaddress.ip_address(d["ip"]))
|
||||
results = [{"ip": ip_str, "mac": "N/A"} for ip_str in alive_ips]
|
||||
return sorted(results, key=lambda d: ipaddress.ip_address(d["ip"]))
|
||||
|
||||
@@ -2,37 +2,40 @@
|
||||
|
||||
## 1. Tổng quan
|
||||
|
||||
Thành phần quét IP trong `scanner.py` dò tìm và liệt kê tất cả thiết bị đang hoạt động trên một dải mạng (ví dụ: `192.168.1.0/24`), trả về danh sách chứa **IP** và **MAC Address** của từng thiết bị.
|
||||
Thành phần quét IP trong `scanner.py` dò tìm và liệt kê tất cả thiết bị **đang thực sự online** trên một dải mạng (ví dụ: `192.168.1.0/24`), trả về danh sách chứa **IP** của từng thiết bị.
|
||||
|
||||
Để đảm bảo tỷ lệ phát hiện cao trên mọi hệ điều hành (Windows, macOS, Linux), scanner kết hợp 3 giai đoạn:
|
||||
Scanner chỉ sử dụng **ICMP Ping** — thiết bị phải phản hồi ping thì mới được liệt kê. Cơ chế này đảm bảo kết quả chính xác, tránh hiện tượng hiển thị thiết bị đã ngắt kết nối do ARP cache cũ trên router/OS.
|
||||
|
||||
1. **Ping Sweep** — đánh thức thiết bị, điền ARP cache
|
||||
2. **Đọc bảng ARP hệ điều hành** (`arp -a`) — không cần quyền Admin
|
||||
3. **Scapy ARP broadcast** — bổ sung thiết bị chặn ICMP
|
||||
|
||||
Bước 2 và 3 chạy **song song** để giảm tổng thời gian scan.
|
||||
> **Lý do loại bỏ ARP và Scapy:** Bảng ARP của hệ điều hành và router giữ cache entry trong nhiều phút, khiến thiết bị đã rút vẫn xuất hiện trong kết quả scan. Ping trực tiếp là phương pháp duy nhất xác nhận thiết bị thực sự đang hoạt động tại thời điểm scan.
|
||||
|
||||
---
|
||||
|
||||
## 2. Luồng hoạt động chính (Hàm `scan_network`)
|
||||
|
||||
```
|
||||
scan_network(network)
|
||||
│
|
||||
├── stage_cb("ping") ← Thông báo UI bắt đầu quét
|
||||
│
|
||||
├── _ping_sweep(network) ← Ping đồng thời toàn bộ host
|
||||
│ │
|
||||
│ ├── _ping_one(ip) × N ← Mỗi host 1 thread
|
||||
│ │
|
||||
│ └── return [alive IPs]
|
||||
│
|
||||
└── return [{"ip": ..., "mac": "N/A"}, ...] ← Sorted by IP
|
||||
```
|
||||
|
||||
**Bước 1 — Ping Sweep**
|
||||
|
||||
- Gọi `_ping_sweep(network)`: gửi ICMP Echo Request đồng thời tới **toàn bộ host** trong dải mạng.
|
||||
- Mỗi thiết bị phản hồi sẽ khiến hệ điều hành **tự ghi MAC vào ARP Cache**.
|
||||
- Sau khi sweep xong, chờ `0.3s` để OS kịp finalize ARP cache (giảm từ 1s trước đây).
|
||||
- Chỉ các IP có `returncode == 0` (phản hồi thành công) mới được đưa vào kết quả.
|
||||
|
||||
**Bước 2 + 3 — ARP Table & Scapy (song song)**
|
||||
|
||||
- Hai hàm `_collect_arp()` và `_collect_scapy()` được submit vào `ThreadPoolExecutor(max_workers=2)` và chạy đồng thời:
|
||||
- `_collect_arp()`: đọc `arp -a`, parse regex lấy IP + MAC.
|
||||
- `_collect_scapy()`: gửi ARP broadcast, nhận phản hồi trực tiếp từ thiết bị.
|
||||
- Kết quả merge theo IP: ARP table làm nền, Scapy bổ sung IP còn thiếu.
|
||||
|
||||
**Bước 4 — Trả về kết quả**
|
||||
**Bước 2 — Trả về kết quả**
|
||||
|
||||
- Danh sách sort tăng dần theo IP:
|
||||
`[{"ip": "192.168.1.2", "mac": "aa:bb:cc:dd:ee:ff"}, ...]`
|
||||
`[{"ip": "192.168.1.2", "mac": "N/A"}, ...]`
|
||||
- Trường `mac` luôn là `"N/A"` — giữ nguyên cấu trúc dict để tương thích với UI và các module khác.
|
||||
|
||||
---
|
||||
|
||||
@@ -40,48 +43,45 @@ Bước 2 và 3 chạy **song song** để giảm tổng thời gian scan.
|
||||
|
||||
### `_ping_one(ip, is_win)`
|
||||
|
||||
Ping một IP đơn lẻ với timeout tối ưu theo nền tảng:
|
||||
Ping một IP đơn lẻ, trả về `True` nếu host phản hồi, `False` nếu không.
|
||||
|
||||
| OS | Lệnh | Timeout wait | Timeout process |
|
||||
| ------- | ------------------ | ----------------- | --------------- |
|
||||
| Windows | `ping -n 1 -w 300` | 300ms | 2s |
|
||||
| macOS | `ping -c 1 -W 500` | 500ms (đơn vị ms) | 2s |
|
||||
| Linux | `ping -c 1 -W 1` | 1s (đơn vị giây) | 2s |
|
||||
Timeout tối ưu theo nền tảng:
|
||||
|
||||
> macOS và Linux dùng cùng flag `-W` nhưng đơn vị khác nhau — được xử lý tách biệt theo `sys.platform`.
|
||||
| OS | Lệnh | Timeout wait | Timeout process |
|
||||
| ------- | ------------------ | ------------ | --------------- |
|
||||
| Windows | `ping -n 1 -w 300` | 300ms | 2s |
|
||||
| macOS | `ping -c 1 -W 300` | 300ms | 2s |
|
||||
| Linux | `ping -c 1 -W 1` | 1s | 2s |
|
||||
|
||||
> macOS và Linux dùng cùng flag `-W` nhưng đơn vị khác nhau (macOS: ms, Linux: giây) — được xử lý tách biệt theo `sys.platform`.
|
||||
|
||||
Windows sử dụng `CREATE_NO_WINDOW` flag để tránh mở cửa sổ console cho mỗi subprocess.
|
||||
|
||||
### `_ping_sweep(network, progress_cb)`
|
||||
|
||||
- Tạo `ThreadPoolExecutor(max_workers=len(hosts))` — **toàn bộ host ping đồng thời**, không batching.
|
||||
- Giới hạn an toàn: chỉ chạy với subnet `/24` trở xuống (`num_addresses <= 256`).
|
||||
- Trả về danh sách IP (string) đã phản hồi thành công.
|
||||
- Gọi `progress_cb(done, total)` sau mỗi ping để UI cập nhật thanh tiến độ.
|
||||
|
||||
### `_collect_arp()` (nội bộ trong `scan_network`)
|
||||
### `scan_network(network, progress_cb, stage_cb)`
|
||||
|
||||
Đọc và parse output `arp -a`, hỗ trợ đa nền tảng:
|
||||
|
||||
- **Windows:** Regex nhận dạng dạng `cc-2d-21-a5-85-b0 dynamic`, chuẩn hóa MAC sang `cc:2d:21:a5:85:b0`.
|
||||
- **macOS/Linux:** Regex nhận dạng dạng `(192.168.1.1) at aa:bb:cc:dd:ee:ff`, bỏ qua entry `(incomplete)`.
|
||||
|
||||
### `_collect_scapy()` (nội bộ trong `scan_network`)
|
||||
|
||||
- Gửi ARP `Who-has` broadcast (`Ether/ARP` qua `srp`) với **timeout 1s** (giảm từ 2s).
|
||||
- Stderr bị redirect tạm thời khi import scapy để tránh spam log ra console.
|
||||
- Tự động bỏ qua nếu scapy không khả dụng (không có Npcap / không có quyền root).
|
||||
- Entry point chính.
|
||||
- `stage_cb("ping")` thông báo UI giai đoạn hiện tại.
|
||||
- Trả về `list[dict]` với format `{"ip": str, "mac": "N/A"}`, sorted theo IP tăng dần.
|
||||
|
||||
---
|
||||
|
||||
## 4. So sánh hiệu năng (trước và sau tối ưu)
|
||||
## 4. Hiệu năng
|
||||
|
||||
| Thay đổi | Trước | Sau |
|
||||
| --------------------------- | --------------------- | ------------------------------------ |
|
||||
| Ping workers | 100 (batching ~3 đợt) | `len(hosts)` (~254, tất cả cùng lúc) |
|
||||
| Ping timeout — Windows | 600ms | 300ms |
|
||||
| Ping timeout — macOS | 1ms (sai đơn vị) | 500ms |
|
||||
| Sleep sau ping sweep | 1.0s | 0.3s |
|
||||
| ARP + Scapy | Tuần tự | **Song song** |
|
||||
| Scapy timeout | 2s | 1s |
|
||||
| **Tổng thời gian scan /24** | ~5–7s | **~1.5–2s** |
|
||||
| Thông số | Giá trị |
|
||||
| --------------------------- | ------------------------------------ |
|
||||
| Ping workers | `len(hosts)` (~254, tất cả cùng lúc) |
|
||||
| Ping timeout — Windows | 300ms |
|
||||
| Ping timeout — macOS | 300ms |
|
||||
| Ping timeout — Linux | 1s |
|
||||
| Process timeout | 2s |
|
||||
| **Tổng thời gian scan /24** | **~1–2s** |
|
||||
|
||||
---
|
||||
|
||||
@@ -89,13 +89,14 @@ Ping một IP đơn lẻ với timeout tối ưu theo nền tảng:
|
||||
|
||||
**Ưu điểm:**
|
||||
|
||||
- Tỷ lệ phát hiện thiết bị cao nhờ kết hợp 3 lớp.
|
||||
- Không cần quyền Admin/Root — ping sweep + ARP table vẫn tìm được ~90% thiết bị.
|
||||
- Tương thích đa nền tảng (Windows/macOS/Linux) qua xử lý riêng từng OS.
|
||||
- ARP table và Scapy chạy song song → không cộng dồn thời gian chờ.
|
||||
- **Kết quả chính xác** — chỉ thiết bị thực sự online mới xuất hiện, không bị ảnh hưởng bởi ARP cache cũ.
|
||||
- Không cần quyền Admin/Root.
|
||||
- Không phụ thuộc thư viện bên ngoài (không cần Scapy, Npcap).
|
||||
- Tương thích đa nền tảng (Windows/macOS/Linux).
|
||||
- Nhanh (~1–2s cho /24) nhờ ping toàn bộ host đồng thời.
|
||||
|
||||
**Nhược điểm:**
|
||||
|
||||
- Vẫn cần `sleep(0.3s)` để OS kịp ghi ARP cache sau ping sweep.
|
||||
- Thiết bị tắt hoàn toàn cả ICMP lẫn ARP sẽ không bị phát hiện.
|
||||
- Spawn ~254 process `ping` đồng thời trên Windows có overhead cao hơn Unix do Windows tạo process chậm hơn.
|
||||
- Không có thông tin MAC address.
|
||||
- Thiết bị chặn ICMP (tắt ping) sẽ không bị phát hiện.
|
||||
- Spawn ~254 process `ping` đồng thời trên Windows có overhead cao hơn Unix.
|
||||
|
||||
194
main.py
194
main.py
@@ -16,7 +16,7 @@ from PyQt6.QtWidgets import (
|
||||
)
|
||||
from PyQt6.QtCore import (
|
||||
Qt, QThread, pyqtSignal, QPropertyAnimation, QAbstractAnimation,
|
||||
QParallelAnimationGroup, QRect
|
||||
QParallelAnimationGroup, QRect, QTimer
|
||||
)
|
||||
from PyQt6.QtGui import QFont, QColor, QIcon, QAction
|
||||
|
||||
@@ -83,7 +83,7 @@ class App(QWidget):
|
||||
title_row.addStretch()
|
||||
copyright_label = QLabel(f"© {datetime.datetime.now().year} Smatec — R&D Software Team. v{get_version()}")
|
||||
copyright_label.setStyleSheet(
|
||||
"color: #9399b2; font-size: 15px; font-weight: 500;"
|
||||
"color: #9399b2; font-size: 13px; font-weight: 500;"
|
||||
)
|
||||
title_row.addWidget(copyright_label)
|
||||
layout.addLayout(title_row)
|
||||
@@ -138,7 +138,7 @@ class App(QWidget):
|
||||
self.fw_label = QLabel("No firmware selected")
|
||||
self.fw_label.setStyleSheet("font-size: 12px; color: #94a3b8; border: none;")
|
||||
fw_card_layout.addWidget(self.fw_label, 1)
|
||||
btn_fw = QPushButton("📁 Chọn FW")
|
||||
btn_fw = QPushButton("📁 Browse FW")
|
||||
btn_fw.setFixedHeight(28)
|
||||
btn_fw.setStyleSheet("""
|
||||
QPushButton {
|
||||
@@ -239,7 +239,7 @@ class App(QWidget):
|
||||
filter_row.addWidget(self.device_count_label)
|
||||
filter_row.addStretch()
|
||||
|
||||
btn_history = QPushButton("📋 Lịch sử nạp")
|
||||
btn_history = QPushButton("📋 Flash History")
|
||||
btn_history.setFixedHeight(24)
|
||||
btn_history.clicked.connect(self._show_history)
|
||||
btn_history.setStyleSheet("background-color: #313244; color: #cdd6f4; font-size: 11px; padding: 2px 8px;")
|
||||
@@ -379,7 +379,7 @@ class App(QWidget):
|
||||
action_row.setSpacing(10)
|
||||
|
||||
par_lbl = QLabel("Concurrent:")
|
||||
par_lbl.setStyleSheet("font-size: 12px; font-weight: bold;")
|
||||
par_lbl.setStyleSheet("font-size: 13px; font-weight: bold; color: #e2e8f0;")
|
||||
action_row.addWidget(par_lbl)
|
||||
|
||||
self.parallel_spin = QSpinBox()
|
||||
@@ -387,9 +387,13 @@ class App(QWidget):
|
||||
self.parallel_spin.setValue(10)
|
||||
self.parallel_spin.setSpecialValueText("∞")
|
||||
self.parallel_spin.setToolTip("0 = unlimited (all at once)")
|
||||
self.parallel_spin.setFixedWidth(65)
|
||||
self.parallel_spin.setFixedHeight(28)
|
||||
self.parallel_spin.setStyleSheet("QSpinBox { font-size: 13px; font-weight: bold; }")
|
||||
self.parallel_spin.setMinimumWidth(80)
|
||||
self.parallel_spin.setFixedHeight(34)
|
||||
self.parallel_spin.setStyleSheet(
|
||||
"QSpinBox { font-size: 14px; font-weight: bold; color: #e2e8f0; "
|
||||
"background-color: #2d3352; border: 1px solid #3d4a6b; border-radius: 6px; padding: 2px 6px; } "
|
||||
"QSpinBox::up-button { width: 18px; } QSpinBox::down-button { width: 18px; }"
|
||||
)
|
||||
action_row.addWidget(self.parallel_spin)
|
||||
|
||||
self.btn_flash = QPushButton("⚡ FLASH SELECTED DEVICES")
|
||||
@@ -404,7 +408,7 @@ class App(QWidget):
|
||||
layout.addWidget(flash_group)
|
||||
|
||||
# ── Automation Button ──
|
||||
self.btn_auto = QPushButton("🤖 Tự động hóa nạp FW")
|
||||
self.btn_auto = QPushButton("🤖 Auto Firmware Flash")
|
||||
self.btn_auto.setObjectName("auto")
|
||||
self.btn_auto.setFixedHeight(32)
|
||||
self.btn_auto.setStyleSheet("""
|
||||
@@ -528,17 +532,17 @@ class App(QWidget):
|
||||
item.setCheckState(Qt.CheckState.Unchecked)
|
||||
|
||||
def _show_history(self):
|
||||
"""Hiển thị lịch sử thiết bị đã nạp trong phiên."""
|
||||
"""Show history of flashed devices in this session."""
|
||||
if not self.flashed_macs:
|
||||
QMessageBox.information(self, "Lịch sử nạp", "Chưa có thiết bị nào được nạp trong phiên này.")
|
||||
QMessageBox.information(self, "Flash History", "No devices have been flashed in this session.")
|
||||
else:
|
||||
lines = []
|
||||
for mac in sorted(self.flashed_macs.keys()):
|
||||
ip, _, result, ts = self.flashed_macs[mac]
|
||||
icon = "✅" if result.startswith("DONE") else "❌"
|
||||
lines.append(f"[{ts}] {icon} {ip} ({mac}) — {result}")
|
||||
msg = f"Lịch sử nạp FW ({len(self.flashed_macs)} thiết bị):\n\n" + "\n".join(lines)
|
||||
QMessageBox.information(self, "Lịch sử nạp", msg)
|
||||
msg = f"Flash History ({len(self.flashed_macs)} device(s)):\n\n" + "\n".join(lines)
|
||||
QMessageBox.information(self, "Flash History", msg)
|
||||
|
||||
# ── Actions ──
|
||||
|
||||
@@ -694,9 +698,9 @@ class App(QWidget):
|
||||
blocked = [dev["ip"] for _, dev in selected if dev["ip"] == "192.168.11.102"]
|
||||
if blocked:
|
||||
QMessageBox.warning(
|
||||
self, "Không được phép",
|
||||
"⚠️ Thiết bị 192.168.11.102 chỉ được nạp ở chế độ Update FW.\n"
|
||||
"Vui lòng bỏ chọn thiết bị này hoặc chuyển sang chế độ Update FW."
|
||||
self, "Not Allowed",
|
||||
"⚠️ Device 192.168.11.102 can only be flashed in Update FW mode.\n"
|
||||
"Please deselect this device or switch to Update FW mode."
|
||||
)
|
||||
return
|
||||
|
||||
@@ -798,10 +802,10 @@ class App(QWidget):
|
||||
QMessageBox.information(self, "Flash Complete", "All devices have been processed.")
|
||||
|
||||
def _open_auto_flash(self):
|
||||
"""Chuyển sang giao diện Tự động hóa nạp FW."""
|
||||
# Sync firmware & network từ main sang auto
|
||||
"""Switch to Auto Flash UI."""
|
||||
# Sync firmware & network from main to auto
|
||||
self.auto_fw_label.setText(
|
||||
os.path.basename(self.firmware) if self.firmware else "Chưa chọn"
|
||||
os.path.basename(self.firmware) if self.firmware else "Not selected"
|
||||
)
|
||||
self.auto_fw_label.setStyleSheet(
|
||||
"color: #a6e3a1; font-weight: bold; font-size: 12px;" if self.firmware
|
||||
@@ -814,14 +818,14 @@ class App(QWidget):
|
||||
self.auto_container.setVisible(True)
|
||||
|
||||
def _back_to_main(self):
|
||||
"""Quay lại giao diện chính."""
|
||||
"""Return to main view."""
|
||||
self.auto_container.setVisible(False)
|
||||
self.main_container.setVisible(True)
|
||||
|
||||
# ── Auto Flash UI Builder ──
|
||||
|
||||
def _build_auto_ui(self):
|
||||
"""Xây dựng giao diện tự động nạp FW bên trong auto_container."""
|
||||
"""Build the Auto Firmware Flash UI inside auto_container."""
|
||||
self.auto_firmware = self.firmware
|
||||
self._auto_worker = None
|
||||
self._auto_device_rows = {}
|
||||
@@ -837,7 +841,7 @@ class App(QWidget):
|
||||
# ── Back button + Title row ──
|
||||
top_row = QHBoxLayout()
|
||||
top_row.setSpacing(8)
|
||||
self.btn_back = QPushButton("⬅ Quay lại")
|
||||
self.btn_back = QPushButton("⬅ Back")
|
||||
self.btn_back.setFixedHeight(32)
|
||||
self.btn_back.setStyleSheet("""
|
||||
QPushButton {
|
||||
@@ -855,14 +859,14 @@ class App(QWidget):
|
||||
self.btn_back.clicked.connect(self._back_to_main)
|
||||
top_row.addWidget(self.btn_back)
|
||||
|
||||
auto_title = QLabel("🤖 Tự động hóa nạp FW")
|
||||
auto_title = QLabel("🤖 Auto Firmware Flash")
|
||||
auto_title.setStyleSheet("font-size: 16px; font-weight: bold; color: #c4b5fd; letter-spacing: 1px;")
|
||||
auto_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
top_row.addWidget(auto_title, 1)
|
||||
auto_layout.addLayout(top_row)
|
||||
|
||||
# ── Config group ──
|
||||
config_group = CollapsibleGroupBox("⚙️ Cấu hình nạp")
|
||||
config_group = CollapsibleGroupBox("⚙️ Flash Configuration")
|
||||
config_layout = QVBoxLayout()
|
||||
config_layout.setSpacing(6)
|
||||
config_layout.setContentsMargins(8, 4, 8, 4)
|
||||
@@ -873,7 +877,7 @@ class App(QWidget):
|
||||
fw_lbl = QLabel("FW:")
|
||||
fw_lbl.setStyleSheet("font-weight: bold; font-size: 12px;")
|
||||
row1.addWidget(fw_lbl)
|
||||
self.auto_fw_label = QLabel(os.path.basename(self.firmware) if self.firmware else "Chưa chọn")
|
||||
self.auto_fw_label = QLabel(os.path.basename(self.firmware) if self.firmware else "Not selected")
|
||||
self.auto_fw_label.setStyleSheet(
|
||||
"color: #a6e3a1; font-weight: bold; font-size: 12px;" if self.firmware
|
||||
else "color: #f38ba8; font-size: 12px;"
|
||||
@@ -882,7 +886,7 @@ class App(QWidget):
|
||||
btn_fw = QPushButton("📁")
|
||||
btn_fw.setFixedSize(40, 30)
|
||||
btn_fw.setStyleSheet("font-size: 16px;")
|
||||
btn_fw.setToolTip("Chọn firmware")
|
||||
btn_fw.setToolTip("Select firmware")
|
||||
btn_fw.clicked.connect(self._auto_select_firmware)
|
||||
row1.addWidget(btn_fw)
|
||||
|
||||
@@ -890,7 +894,7 @@ class App(QWidget):
|
||||
sep1.setStyleSheet("color: #3d4a6b; font-size: 14px;")
|
||||
row1.addWidget(sep1)
|
||||
|
||||
net_lbl = QLabel("Mạng:")
|
||||
net_lbl = QLabel("Network:")
|
||||
net_lbl.setStyleSheet("font-weight: bold; font-size: 12px;")
|
||||
row1.addWidget(net_lbl)
|
||||
self.auto_net_input = QLineEdit(get_default_network(self.local_ip))
|
||||
@@ -906,46 +910,66 @@ class App(QWidget):
|
||||
row2 = QHBoxLayout()
|
||||
row2.setSpacing(12)
|
||||
|
||||
cnt_lbl = QLabel("Số lượng:")
|
||||
cnt_lbl.setStyleSheet("font-weight: bold; font-size: 12px;")
|
||||
cnt_lbl = QLabel("Target Count:")
|
||||
cnt_lbl.setStyleSheet("font-weight: bold; font-size: 13px; color: #e2e8f0;")
|
||||
row2.addWidget(cnt_lbl)
|
||||
self.auto_target_spin = QSpinBox()
|
||||
self.auto_target_spin.setRange(1, 500)
|
||||
self.auto_target_spin.setValue(5)
|
||||
self.auto_target_spin.setFixedWidth(70)
|
||||
self.auto_target_spin.setFixedHeight(28)
|
||||
self.auto_target_spin.setStyleSheet("QSpinBox { font-size: 13px; font-weight: bold; }")
|
||||
self.auto_target_spin.setMinimumWidth(80)
|
||||
self.auto_target_spin.setFixedHeight(34)
|
||||
self.auto_target_spin.setStyleSheet(
|
||||
"QSpinBox { font-size: 14px; font-weight: bold; color: #e2e8f0; "
|
||||
"background-color: #2d3352; border: 1px solid #3d4a6b; border-radius: 6px; padding: 2px 6px; } "
|
||||
"QSpinBox::up-button { width: 18px; } QSpinBox::down-button { width: 18px; }"
|
||||
)
|
||||
row2.addWidget(self.auto_target_spin)
|
||||
|
||||
sep2 = QLabel("│")
|
||||
sep2.setStyleSheet("color: #3d4a6b; font-size: 14px;")
|
||||
row2.addWidget(sep2)
|
||||
|
||||
meth_lbl = QLabel("Phương thức:")
|
||||
meth_lbl = QLabel("Method:")
|
||||
meth_lbl.setStyleSheet("font-weight: bold; font-size: 12px;")
|
||||
row2.addWidget(meth_lbl)
|
||||
self.auto_method_combo = QComboBox()
|
||||
self.auto_method_combo.addItem("🌐 API (LuCI)", "api")
|
||||
self.auto_method_combo.addItem("💻 SSH", "ssh")
|
||||
self.auto_method_combo.setFixedHeight(28)
|
||||
self.auto_method_combo.setFixedHeight(34)
|
||||
self.auto_method_combo.setMinimumWidth(140)
|
||||
self.auto_method_combo.setStyleSheet("""
|
||||
QComboBox {
|
||||
background-color: #2d3352; border: 1px solid #3d4a6b; border-radius: 6px;
|
||||
padding: 2px 8px; color: #e2e8f0; font-size: 13px; font-weight: bold;
|
||||
}
|
||||
QComboBox:hover { border-color: #7eb8f7; }
|
||||
QComboBox::drop-down { border: none; width: 20px; }
|
||||
QComboBox QAbstractItemView {
|
||||
background-color: #2d3352; color: #e2e8f0;
|
||||
border: 1px solid #3d4a6b; selection-background-color: #3d4a6b;
|
||||
}
|
||||
""")
|
||||
row2.addWidget(self.auto_method_combo)
|
||||
|
||||
sep3 = QLabel("│")
|
||||
sep3.setStyleSheet("color: #3d4a6b; font-size: 14px;")
|
||||
row2.addWidget(sep3)
|
||||
|
||||
par_lbl = QLabel("Song song:")
|
||||
par_lbl.setStyleSheet("font-weight: bold; font-size: 12px;")
|
||||
par_lbl = QLabel("Concurrent:")
|
||||
par_lbl.setStyleSheet("font-weight: bold; font-size: 13px; color: #e2e8f0;")
|
||||
row2.addWidget(par_lbl)
|
||||
self.auto_parallel_spin = QSpinBox()
|
||||
self.auto_parallel_spin.setRange(0, 100)
|
||||
self.auto_parallel_spin.setValue(10)
|
||||
self.auto_parallel_spin.setSpecialValueText("∞")
|
||||
self.auto_parallel_spin.setToolTip("0 = 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; }")
|
||||
self.auto_parallel_spin.setToolTip("0 = unlimited")
|
||||
self.auto_parallel_spin.setMinimumWidth(80)
|
||||
self.auto_parallel_spin.setFixedHeight(34)
|
||||
self.auto_parallel_spin.setStyleSheet(
|
||||
"QSpinBox { font-size: 14px; font-weight: bold; color: #e2e8f0; "
|
||||
"background-color: #2d3352; border: 1px solid #3d4a6b; border-radius: 6px; padding: 2px 6px; } "
|
||||
"QSpinBox::up-button { width: 18px; } QSpinBox::down-button { width: 18px; }"
|
||||
)
|
||||
row2.addWidget(self.auto_parallel_spin)
|
||||
row2.addStretch()
|
||||
config_layout.addLayout(row2)
|
||||
@@ -956,7 +980,7 @@ class App(QWidget):
|
||||
# ── Control Buttons ──
|
||||
btn_row = QHBoxLayout()
|
||||
btn_row.setSpacing(8)
|
||||
self.auto_btn_start = QPushButton("▶ XÁC NHẬN & BẮT ĐẦU")
|
||||
self.auto_btn_start = QPushButton("▶ CONFIRM & START")
|
||||
self.auto_btn_start.setObjectName("start_btn")
|
||||
self.auto_btn_start.setFixedHeight(36)
|
||||
self.auto_btn_start.setStyleSheet("""
|
||||
@@ -975,7 +999,7 @@ class App(QWidget):
|
||||
self.auto_btn_start.clicked.connect(self._auto_on_start)
|
||||
btn_row.addWidget(self.auto_btn_start)
|
||||
|
||||
self.auto_btn_stop = QPushButton("⏹ DỪNG")
|
||||
self.auto_btn_stop = QPushButton("⏹ STOP")
|
||||
self.auto_btn_stop.setObjectName("stop_btn")
|
||||
self.auto_btn_stop.setFixedHeight(36)
|
||||
self.auto_btn_stop.setEnabled(False)
|
||||
@@ -999,7 +1023,7 @@ class App(QWidget):
|
||||
# ── Status + Progress ──
|
||||
status_row = QHBoxLayout()
|
||||
status_row.setSpacing(8)
|
||||
self.auto_status_label = QLabel("⏸ Chờ bắt đầu...")
|
||||
self.auto_status_label = QLabel("⏸ Waiting to start...")
|
||||
self.auto_status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #94a3b8;")
|
||||
status_row.addWidget(self.auto_status_label, 1)
|
||||
|
||||
@@ -1012,14 +1036,14 @@ class App(QWidget):
|
||||
auto_layout.addLayout(status_row)
|
||||
|
||||
# ── Device Table ──
|
||||
dev_group = QGroupBox("📋 Danh sách thiết bị")
|
||||
dev_group = QGroupBox("📋 Device List")
|
||||
dev_layout = QVBoxLayout()
|
||||
dev_layout.setSpacing(2)
|
||||
dev_layout.setContentsMargins(4, 12, 4, 4)
|
||||
|
||||
self.auto_result_table = QTableWidget()
|
||||
self.auto_result_table.setColumnCount(4)
|
||||
self.auto_result_table.setHorizontalHeaderLabels(["#", "IP", "MAC", "Kết quả"])
|
||||
self.auto_result_table.setHorizontalHeaderLabels(["#", "IP", "MAC", "Result"])
|
||||
self.auto_result_table.setAlternatingRowColors(True)
|
||||
header = self.auto_result_table.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||
@@ -1040,7 +1064,7 @@ class App(QWidget):
|
||||
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 = QPushButton("📋 Flash History")
|
||||
btn_auto_history.setFixedHeight(24)
|
||||
btn_auto_history.setStyleSheet("background-color: #313244; color: #cdd6f4; font-size: 11px; padding: 2px 8px;")
|
||||
btn_auto_history.clicked.connect(self._show_auto_history)
|
||||
@@ -1060,14 +1084,13 @@ class App(QWidget):
|
||||
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;"
|
||||
"color: #cdd6f4; font-size: 12px; font-family: 'Consolas', 'Courier New', monospace;"
|
||||
"padding: 6px; background-color: #11121d; border-radius: 4px;"
|
||||
)
|
||||
self.auto_log_content.setTextFormat(Qt.TextFormat.PlainText)
|
||||
self.auto_log_area.setWidget(self.auto_log_content)
|
||||
self.auto_log_area.setWidgetResizable(True)
|
||||
self.auto_log_area.setMinimumHeight(120)
|
||||
self.auto_log_area.setMaximumHeight(280)
|
||||
self.auto_log_area.setMinimumHeight(150)
|
||||
self.auto_log_area.setStyleSheet("QScrollArea { border: 1px solid #2d3748; border-radius: 4px; background-color: #11121d; }")
|
||||
log_layout.addWidget(self.auto_log_area)
|
||||
|
||||
@@ -1078,18 +1101,18 @@ class App(QWidget):
|
||||
|
||||
def _show_auto_history(self):
|
||||
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.")
|
||||
QMessageBox.information(self, "Flash History", "No devices have been flashed in this session.")
|
||||
return
|
||||
lines = []
|
||||
for ip, mac, result, ts in self._auto_history:
|
||||
icon = "✅" if result.startswith("DONE") else "❌"
|
||||
lines.append(f"[{ts}] {icon} {ip} ({mac}) — {result}")
|
||||
msg = f"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)
|
||||
msg = f"Flash History ({len(self._auto_history)} device(s)):\n\n" + "\n".join(lines)
|
||||
QMessageBox.information(self, "Flash History", msg)
|
||||
|
||||
def _auto_select_firmware(self):
|
||||
file, _ = QFileDialog.getOpenFileName(
|
||||
self, "Chọn Firmware", "",
|
||||
self, "Select Firmware", "",
|
||||
"Firmware Files (*.bin *.hex *.uf2);;All Files (*)"
|
||||
)
|
||||
if file:
|
||||
@@ -1107,19 +1130,20 @@ class App(QWidget):
|
||||
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())
|
||||
QTimer.singleShot(0, lambda: self.auto_log_area.verticalScrollBar().setValue(
|
||||
self.auto_log_area.verticalScrollBar().maximum()
|
||||
))
|
||||
|
||||
def _auto_on_start(self):
|
||||
if not self.auto_firmware:
|
||||
QMessageBox.warning(self, "Chưa chọn FW", "Vui lòng chọn file firmware trước.")
|
||||
QMessageBox.warning(self, "No Firmware Selected", "Please select a firmware file first.")
|
||||
return
|
||||
|
||||
network_str = self.auto_net_input.text().strip()
|
||||
try:
|
||||
ipaddress.ip_network(network_str, strict=False)
|
||||
except ValueError:
|
||||
QMessageBox.warning(self, "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")
|
||||
QMessageBox.warning(self, "Invalid Network", f"'{network_str}' is not a valid network.\nExample: 192.168.4.0/24")
|
||||
return
|
||||
|
||||
target = self.auto_target_spin.value()
|
||||
@@ -1127,13 +1151,13 @@ class App(QWidget):
|
||||
max_workers = self.auto_parallel_spin.value()
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self, "Xác nhận",
|
||||
f"Bắt đầu tự động nạp FW?\n\n"
|
||||
self, "Confirm",
|
||||
f"Start auto firmware flash?\n\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"
|
||||
f" Song song: {max_workers if max_workers > 0 else 'Không giới hạn'}\n",
|
||||
f" Network: {network_str}\n"
|
||||
f" Target count: {target} device(s)\n"
|
||||
f" Method: {method.upper()}\n"
|
||||
f" Concurrent: {max_workers if max_workers > 0 else 'Unlimited'}\n",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.Yes,
|
||||
)
|
||||
@@ -1159,7 +1183,7 @@ class App(QWidget):
|
||||
self.auto_parallel_spin.setEnabled(False)
|
||||
self.btn_back.setEnabled(False)
|
||||
|
||||
self.auto_status_label.setText("🔍 Đang scan mạng LAN...")
|
||||
self.auto_status_label.setText("🔍 Scanning LAN...")
|
||||
self.auto_status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #f9e2af;")
|
||||
|
||||
self._auto_worker = AutoFlashWorker(
|
||||
@@ -1186,13 +1210,13 @@ class App(QWidget):
|
||||
if self._auto_worker:
|
||||
self._auto_worker.stop()
|
||||
self.auto_btn_stop.setEnabled(False)
|
||||
self.auto_status_label.setText("⏳ Đang dừng...")
|
||||
self.auto_status_label.setText("⏳ Stopping...")
|
||||
|
||||
def _auto_on_scan_found(self, count):
|
||||
target = self.auto_target_spin.value()
|
||||
self.auto_status_label.setText(f"🔍 Scan: tìm thấy {count}/{target} thiết bị...")
|
||||
self.auto_status_label.setText(f"🔍 Scan: found {count}/{target} device(s)...")
|
||||
if count >= target:
|
||||
self.auto_status_label.setText(f"⚡ Đủ {target} thiết bị — đang nạp FW...")
|
||||
self.auto_status_label.setText(f"⚡ {target} device(s) found — flashing firmware...")
|
||||
self.auto_status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #a6e3a1;")
|
||||
|
||||
def _auto_on_devices_ready(self, devices):
|
||||
@@ -1205,11 +1229,11 @@ class App(QWidget):
|
||||
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 = QTableWidgetItem("⏳ Waiting...")
|
||||
waiting_item.setForeground(QColor("#94a3b8"))
|
||||
self.auto_result_table.setItem(i, 3, waiting_item)
|
||||
self._auto_device_rows[dev["ip"]] = i
|
||||
self.auto_summary_label.setText(f"Tổng: {len(devices)} thiết bị")
|
||||
self.auto_summary_label.setText(f"Total: {len(devices)} device(s)")
|
||||
|
||||
def _auto_on_device_status(self, ip, msg):
|
||||
row = self._auto_device_rows.get(ip)
|
||||
@@ -1245,42 +1269,42 @@ class App(QWidget):
|
||||
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}"
|
||||
f"Total: {total} | Done: {done} | ✅ {self._auto_success_count} | ❌ {self._auto_fail_count}"
|
||||
)
|
||||
|
||||
def _auto_on_flash_progress(self, done, total):
|
||||
self.auto_progress_bar.setVisible(True)
|
||||
self.auto_progress_bar.setMaximum(total)
|
||||
self.auto_progress_bar.setValue(done)
|
||||
self.auto_status_label.setText(f"⚡ Nạp FW: {done}/{total} thiết bị...")
|
||||
self.auto_status_label.setText(f"⚡ Flashing: {done}/{total} device(s)...")
|
||||
|
||||
def _auto_on_all_done(self, success, fail):
|
||||
self._auto_reset_controls()
|
||||
self.auto_status_label.setText(f"🏁 Hoàn thành! ✅ {success} | ❌ {fail}")
|
||||
self.auto_status_label.setText(f"🏁 Complete! ✅ {success} | ❌ {fail}")
|
||||
self.auto_status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #a6e3a1;")
|
||||
QMessageBox.information(
|
||||
self, "Hoàn thành",
|
||||
f"Tự động nạp FW hoàn thành!\n\n"
|
||||
f"✅ Thành công: {success}\n"
|
||||
f"❌ Thất bại: {fail}",
|
||||
self, "Complete",
|
||||
f"Auto firmware flash complete!\n\n"
|
||||
f"✅ Success: {success}\n"
|
||||
f"❌ Failed: {fail}",
|
||||
)
|
||||
|
||||
def _auto_on_stopped(self):
|
||||
self._auto_reset_controls()
|
||||
self.auto_status_label.setText("⛔ Đã dừng bởi người dùng")
|
||||
self.auto_status_label.setText("⛔ Stopped by user")
|
||||
self.auto_status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #f38ba8;")
|
||||
|
||||
def _auto_on_scan_timeout(self, found, target):
|
||||
self._auto_reset_controls()
|
||||
self.auto_status_label.setText(f"⚠️ Scan hết lần: chỉ tìm thấy {found}/{target} thiết bị")
|
||||
self.auto_status_label.setText(f"⚠️ Scan timed out: only found {found}/{target} device(s)")
|
||||
self.auto_status_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #fab387;")
|
||||
QMessageBox.warning(
|
||||
self, "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.auto_net_input.text()}) có đúng không\n"
|
||||
f" • Thử lại sau khi kiểm tra",
|
||||
self, "Not Enough Devices",
|
||||
f"Scan reached maximum attempts but only found {found}/{target} device(s).\n\n"
|
||||
f"Please check:\n"
|
||||
f" • Devices are powered on and connected to the network\n"
|
||||
f" • Network range ({self.auto_net_input.text()}) is correct\n"
|
||||
f" • Try again after verifying",
|
||||
)
|
||||
|
||||
def _auto_reset_controls(self):
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.2.1
|
||||
1.2.2
|
||||
|
||||
Reference in New Issue
Block a user