# ⚡ IoT 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ị IoT (ESP32/ESP8266) trong mạng LAN. > **Tech stack:** Python 3.9+ · PyQt6 · Scapy · Requests --- ## 📁 Cấu trúc dự án ``` iot_fw_loader/ ├── main.py # UI chính (PyQt6) + điều phối toàn bộ luồng ├── scanner.py # Quét thiết bị trong mạng LAN ├── device_filter.py # Lọc thiết bị IoT theo MAC vendor ├── flasher.py # Upload firmware qua HTTP ├── run.sh # Script khởi chạy (macOS/Linux) ├── run.bat # Script khởi chạy (Windows) └── venv/ # Python virtual environment ``` --- ## 🔧 Cài đặt & Chạy ### Yêu cầu - Python 3.9+ - Các thư viện: `PyQt6`, `scapy`, `requests` ### Khởi chạy nhanh **macOS / Linux:** ```bash ./run.sh ``` **Windows:** ```bat run.bat ``` > Script tự tạo `venv` và cài dependencies nếu chưa có. **Hoặc chạy thủ công:** ```bash source venv/bin/activate python main.py ``` --- ## 🏗 Kiến trúc hệ thống ``` ┌─────────────────────────────────────────────────────┐ │ main.py (UI) │ │ ┌───────────┐ ┌───────────┐ ┌────────────────────┐ │ │ │ Machine │ │ Firmware │ │ Network Scan │ │ │ │ Info │ │ Selector │ │ (QThread) │ │ │ └───────────┘ └───────────┘ └────────┬───────────┘ │ │ ┌────────────────────────────────────┼───────────┐ │ │ │ Device Table │ │ │ │ │ IP │ MAC │ Type │ Status │ │ │ │ └────────────────────────────────────┼───────────┘ │ │ ┌────────────────────────────────────┼───────────┐ │ │ │ Flash Controls + Progress Bar │ │ │ │ │ (ThreadPoolExecutor × 5) │ │ │ │ └────────────────────────────────────┼───────────┘ │ └───────────────────────────────────────┼─────────────┘ │ ┌───────────────────┼───────────────────┐ │ │ │ ┌─────▼─────┐ ┌──────▼──────┐ ┌──────▼──────┐ │ scanner.py │ │device_filter│ │ flasher.py │ │ │ │ .py │ │ │ │ Scapy ARP │ │ MAC vendor │ │ HTTP POST │ │ ↓ fallback│ │ matching │ │ /update │ │ arp -a │ │ │ │ │ └────────────┘ └─────────────┘ └─────────────┘ ``` --- ## 🔄 Luồng hoạt động chi tiết ### Tổng quan ``` Khởi động App ──▶ Hiển thị Machine Info │ ▼ Chọn Firmware (.bin/.hex/.uf2) │ ▼ Nhập dải mạng ──▶ Nhấn "Scan LAN" │ ▼ ┌─────────────────────────────────┐ │ ScanThread (background) │ │ 1. Thử Scapy ARP broadcast │ │ 2. Nếu lỗi → fallback arp -a │ └──────────────┬──────────────────┘ ▼ Lọc IoT device theo MAC vendor │ ▼ Hiển thị bảng: tất cả thiết bị (IoT = 🟢 xanh lá, Other = ⚪) │ ▼ Nhấn "Flash All IoT Devices" │ ▼ ┌──────────────────────────────────┐ │ ThreadPoolExecutor (5 workers) │ │ POST firmware → mỗi thiết bị │ │ Cập nhật status + progress bar │ └──────────────────────────────────┘ │ ▼ Hoàn tất ✅ ``` --- ## 📋 Phân tích từng module ### 1. `main.py` — Giao diện & Điều phối | Thành phần | Mô tả | | ------------------------- | --------------------------------------------- | | `get_local_ip()` | Lấy IP máy tính qua UDP socket tới `8.8.8.8` | | `get_default_network(ip)` | Tự tính dải mạng `/24` từ IP máy | | `get_machine_info()` | Thu thập hostname, IP, OS, MAC | | `ScanThread` | `QThread` chạy scan ở background, tránh đơ UI | | `App` | Widget chính chứa toàn bộ UI | **Các phần UI:** | Group Box | Chức năng | | ---------------- | -------------------------------------- | | 🖥 Machine Info | Hiển thị Hostname, IP, OS, MAC của máy | | 📦 Firmware | Chọn file firmware | | 📡 Network Scan | Nhập dải mạng + nút Scan | | 📋 Devices Found | Bảng thiết bị (IP, MAC, Type, Status) | | 🚀 Flash | Progress bar + nút Flash All | **Xử lý sự kiện chính:** | Method | Trigger | Hành động | | ------------------ | ------------------ | ------------------------------------------ | | `select_fw()` | Nhấn "Select File" | Mở dialog chọn firmware | | `scan()` | Nhấn "Scan LAN" | Tạo `ScanThread` → chạy background | | `_on_scan_done()` | Scan hoàn tất | Phân loại IoT/Other → hiển thị bảng | | `_on_scan_error()` | Scan lỗi | Hiện `QMessageBox` lỗi | | `flash_all()` | Nhấn "Flash All" | Tạo `ThreadPoolExecutor` → flash song song | | `flash_worker()` | Mỗi thread | POST firmware → cập nhật status | --- ### 2. `scanner.py` — Quét mạng **Chiến lược 2 lớp (dual-layer scan):** | Phương pháp | Hàm | Yêu cầu | Độ chính xác | | ----------------------- | ------------------------ | -------------- | ------------------------------------------- | | **Primary:** Scapy ARP | `_scan_with_scapy()` | Root/sudo | Cao — scan active | | **Fallback:** ARP table | `_scan_with_arp_table()` | Không cần root | Trung bình — chỉ thấy thiết bị đã giao tiếp | **Luồng trong `scan_network()`:** ```python try: return _scan_with_scapy(network) # Cần root except: return _scan_with_arp_table(network) # Fallback, parse "arp -a" ``` **Scapy ARP scan:** - Tạo ARP request → broadcast `ff:ff:ff:ff:ff:ff` - Thu thập response → trích IP + MAC - Timeout: 2 giây **ARP table fallback:** - Chạy lệnh `arp -a` của hệ thống - Parse output bằng regex: `\(IP\) at MAC` - Lọc theo dải mạng đầu vào --- ### 3. `device_filter.py` — Phân loại thiết bị Lọc thiết bị IoT dựa trên **3 byte đầu của MAC address** (OUI — Organizationally Unique Identifier): | MAC Prefix | Vendor | | ---------- | ------------------------- | | `24:6F:28` | Espressif (ESP32/ESP8266) | | `84:F3:EB` | Espressif | | `DC:4F:22` | Espressif | > **Lưu ý:** Chỉ thiết bị có MAC bắt đầu bằng các prefix trên mới được đánh dấu là IoT. Tất cả thiết bị khác vẫn hiển thị trong bảng với label "Other". --- ### 4. `flasher.py` — Nạp firmware | Bước | Chi tiết | | ------------- | --------------------------------------------------- | | 1. Mở file | `open(firmware_path, "rb")` | | 2. Upload | `POST http://{ip}/update` với `multipart/form-data` | | 3. Chờ reboot | `time.sleep(3)` — đợi thiết bị khởi động lại | | 4. Kết quả | Trả `"DONE"` hoặc `"FAIL"` | > **Giả định:** Thiết bị IoT chạy HTTP server với endpoint `/update` hỗ trợ OTA firmware update (chuẩn ESP32 Arduino OTA). --- ## ⚙️ Cấu hình | Tham số | Giá trị | Vị trí | | ----------------- | ----------------------- | ----------------------------------------------- | | Network range | Tự tính `/24` từ IP máy | `main.py` → `get_default_network()` | | Scan timeout | 2 giây | `scanner.py` → `srp(..., timeout=2)` | | Flash timeout | 10 giây | `flasher.py` → `requests.post(..., timeout=10)` | | Reboot wait | 3 giây | `flasher.py` → `time.sleep(3)` | | Max flash workers | 5 | `main.py` → `ThreadPoolExecutor(max_workers=5)` | | Firmware filter | `.bin .hex .uf2` | `main.py` → `QFileDialog` filter | --- ## 🔒 Lưu ý bảo mật & Quyền - **Scapy cần root/sudo** trên macOS/Linux để gửi raw ARP packets - Nếu không có quyền root → tự động dùng `arp -a` (không cần root nhưng chỉ thấy thiết bị đã giao tiếp gần đây) - Firmware upload qua **HTTP không mã hóa** — chỉ nên dùng trong mạng LAN nội bộ --- ## 📊 Trạng thái thiết bị (State Machine) ``` READY ──────▶ FLASHING │ │ │ ┌─────┴──────┐ │ ▼ ▼ │ ✅ DONE ❌ FAIL │ └── (thiết bị Other: trạng thái "—", không flash) ```