Thêm tính năng tự động hóa nạp FW

This commit is contained in:
2026-03-09 17:20:07 +07:00
parent 56b688766e
commit 47f3320c3d
5 changed files with 1356 additions and 184 deletions

200
core/auto_flash_worker.py Normal file
View File

@@ -0,0 +1,200 @@
"""
Worker thread cho chế độ "Tự động hóa nạp FW".
Flow:
1. Scan mạng LAN liên tục (tối đa max_scan_rounds lần)
2. Khi phát hiện đủ số thiết bị yêu cầu → bắt đầu nạp FW
3. Nạp FW theo phương thức đã chọn (API / SSH), tự động retry nếu lỗi
4. Thông báo khi hoàn thành
"""
import time
import ipaddress
from PyQt6.QtCore import QThread, pyqtSignal
from concurrent.futures import ThreadPoolExecutor
from core.scanner import scan_network
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ị
class AutoFlashWorker(QThread):
"""Tự động scan → flash khi đủ số lượng thiết bị."""
# Signals
log_message = pyqtSignal(str) # log message cho UI
scan_found = pyqtSignal(int) # số device tìm thấy trong lần scan hiện tại
devices_ready = pyqtSignal(list) # danh sách devices sẵn sàng flash [{ip, mac}, ...]
device_status = pyqtSignal(str, str) # ip, status message
device_done = pyqtSignal(str, str, str) # ip, mac, result ("DONE"/"FAIL:...")
flash_progress = pyqtSignal(int, int) # done_count, total
all_done = pyqtSignal(int, int) # success_count, fail_count
scan_timeout = pyqtSignal(int, int) # found_count, target_count — scan hết lần mà chưa đủ
stopped = pyqtSignal() # khi dừng bởi user
def __init__(self, network, target_count, method, max_workers,
firmware_path, local_ip="", gateway_ip="",
ssh_user="root", ssh_password="admin123a",
ssh_backup_password="admin123a", set_passwd=True):
super().__init__()
self.network = network
self.target_count = target_count
self.method = method
self.max_workers = max_workers
self.firmware_path = firmware_path
self.local_ip = local_ip
self.gateway_ip = gateway_ip
self.ssh_user = ssh_user
self.ssh_password = ssh_password
self.ssh_backup_password = ssh_backup_password
self.set_passwd = set_passwd
self._stop_flag = False
def stop(self):
self._stop_flag = True
def run(self):
self.log_message.emit("🚀 Bắt đầu chế độ tự động hóa nạp FW...")
self.log_message.emit(f" Mục tiêu: {self.target_count} thiết bị | Phương thức: {self.method.upper()} | Song song: {self.max_workers}")
self.log_message.emit(f" Mạng: {self.network}")
self.log_message.emit("")
# ── Phase 1: Scan liên tục cho đến khi đủ thiết bị (tối đa MAX_SCAN_ROUNDS lần) ──
devices = []
scan_round = 0
excluded = {self.local_ip, self.gateway_ip}
best_found = 0
while not self._stop_flag:
scan_round += 1
self.log_message.emit(f"🔍 Scan lần {scan_round}/{MAX_SCAN_ROUNDS}...")
try:
results = scan_network(str(self.network))
except Exception as e:
self.log_message.emit(f"❌ Scan thất bại: {e}")
if self._stop_flag:
break
time.sleep(3)
continue
# Lọc bỏ gateway, local IP, và 192.168.11.102 (chỉ update mới được nạp)
filtered = [d for d in results if d["ip"] not in excluded and d["ip"] != "192.168.11.102"]
found_count = len(filtered)
best_found = max(best_found, found_count)
self.scan_found.emit(found_count)
self.log_message.emit(f" Tìm thấy {found_count}/{self.target_count} thiết bị")
if found_count >= self.target_count:
# Chỉ lấy đúng số lượng yêu cầu
devices = filtered[:self.target_count]
self.log_message.emit(f"✅ Đủ {self.target_count} thiết bị! Bắt đầu nạp FW...")
self.log_message.emit("")
break
if self._stop_flag:
break
# Kiểm tra đã scan quá số lần tối đa
if scan_round >= MAX_SCAN_ROUNDS:
self.log_message.emit(f"⚠️ Đã scan {MAX_SCAN_ROUNDS} lần mà chỉ tìm thấy {best_found}/{self.target_count} thiết bị.")
self.scan_timeout.emit(best_found, self.target_count)
self.stopped.emit()
return
self.log_message.emit(f" Chưa đủ, chờ 5 giây rồi scan lại...")
# Chờ 5 giây nhưng check stop flag mỗi 0.5s
for _ in range(10):
if self._stop_flag:
break
time.sleep(0.5)
if self._stop_flag:
self.log_message.emit("⛔ Đã dừng bởi người dùng.")
self.stopped.emit()
return
# ── Phase 2: Flash ──
total = len(devices)
success_count = 0
fail_count = 0
done_count = 0
self.flash_progress.emit(0, total)
# Gửi danh sách devices cho UI để populate bảng trước khi flash
self.devices_ready.emit(devices)
# Log danh sách thiết bị
for d in devices:
self.log_message.emit(f" 📱 {d['ip']} ({d['mac']})")
self.log_message.emit("")
def _flash_one(dev):
nonlocal success_count, fail_count, done_count
ip = dev["ip"]
mac = dev.get("mac", "N/A")
result = ""
for attempt in range(1, MAX_FLASH_RETRIES + 1):
if self._stop_flag:
result = "FAIL: Dừng bởi người dùng"
break
if attempt > 1:
self.log_message.emit(f"🔄 [{ip}] Retry lần {attempt}/{MAX_FLASH_RETRIES}...")
self.device_status.emit(ip, f"Retry lần {attempt}/{MAX_FLASH_RETRIES}...")
time.sleep(2) # chờ thiết bị ổn định trước khi retry
try:
def on_status(msg):
self.device_status.emit(ip, msg)
if self.method == "ssh":
result = flash_device_new_ssh(
ip, self.firmware_path,
user=self.ssh_user,
password=self.ssh_password,
backup_password=self.ssh_backup_password,
set_passwd=self.set_passwd,
status_cb=on_status,
)
else:
result = flash_device_api(
ip, self.firmware_path,
status_cb=on_status,
)
except Exception as e:
result = f"FAIL: {e}"
if result.startswith("DONE"):
break
else:
if attempt < MAX_FLASH_RETRIES:
self.log_message.emit(f"⚠️ [{ip}] Lần {attempt} thất bại: {result}")
else:
self.log_message.emit(f"❌ [{ip}] Thất bại sau {MAX_FLASH_RETRIES} lần thử: {result}")
self.device_done.emit(ip, mac, result)
if result.startswith("DONE"):
success_count += 1
else:
fail_count += 1
done_count += 1
self.flash_progress.emit(done_count, total)
workers = self.max_workers if self.max_workers > 0 else total
with ThreadPoolExecutor(max_workers=max(workers, 1)) as executor:
futures = [executor.submit(_flash_one, dev) for dev in devices]
for f in futures:
f.result()
if self._stop_flag:
break
self.log_message.emit("")
self.log_message.emit(f"🏁 Hoàn thành! Thành công: {success_count} | Thất bại: {fail_count}")
self.all_done.emit(success_count, fail_count)