Thêm tính năng tự động hóa nạp FW
This commit is contained in:
200
core/auto_flash_worker.py
Normal file
200
core/auto_flash_worker.py
Normal 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)
|
||||
Reference in New Issue
Block a user