Skip to content

Add Demo Mode via Service Layer with FastAPI Dependency Injection#25

Draft
Copilot wants to merge 6 commits intomainfrom
copilot/add-demo-mode-implementation
Draft

Add Demo Mode via Service Layer with FastAPI Dependency Injection#25
Copilot wants to merge 6 commits intomainfrom
copilot/add-demo-mode-implementation

Conversation

Copy link

Copilot AI commented Feb 23, 2026

Introduces a service abstraction layer so the WebUI can run against simulated data without Raspberry Pi hardware, toggled via HACKMASTER_DEMO_MODE=true. Uses FastAPI's dependency_overrides — no router code changes needed to switch modes.

Architecture

  • ABC interfaces (app/services/wifi_service.py, ble_service.py, ir_service.py, rfid_service.py) — define the contract each module must fulfill
  • Real implementations (app/services/real/) — hardware logic migrated from routers; module-level globals preserve state across requests (same semantics as before)
  • Mock implementations (app/services/mock/) — return schema-compatible fake data (e.g. 3 fake APs, captured handshake, cracked password, EVIL_TWIN defense issue)

Router changes

All four routers (WiFi.py, BLE.py, IR.py, RFID.py) now expose a get_*_service() factory and inject it via Depends. HTML page routes are untouched.

def get_wifi_service() -> WiFiService:
    return WiFiRealService()

@router.get("/interface/details")
async def list_adapters(service: WiFiService = Depends(get_wifi_service)):
    return await service.get_interface_details()

Demo mode wiring (main.py)

DEMO_MODE = os.getenv("HACKMASTER_DEMO_MODE", "false").lower() == "true"

if DEMO_MODE:
    _wifi_mock = WiFiMockService()
    app.dependency_overrides[WiFi.get_wifi_service] = lambda: _wifi_mock
    # ... same for BLE, IR, RFID

Singleton mock instances are used so stateful operations (e.g. IR recording state machine) behave consistently across requests.

Bug fixes (incidental)

  • BLE.py: duplicate function name read_airpods_emulator (lines 29 and 117) — second renamed to read_airpods_emulator_page
  • IR.py: random was used in enumerate_ir_code without being imported — moved to ir_real.py with the implementation
  • wifi_real.py: operator precedence bug in adapter name filtering condition
  • ir_real.py: potential IndexError when function is an empty string in generate_ir_code

New files

  • app/.env.example — documents HACKMASTER_DEMO_MODE
  • __pycache__/ and *.pyc added to .gitignore
Original prompt

目標

為 WebUI 建立一個可以回應模擬資料的 Demo 模式,同時確保未來 API 介面變動時 Mock 與真實實作不會出現不一致。利用 FastAPI 原生 Dependency Injection (Depends + dependency_overrides) 機制實現,透過環境變數 HACKMASTER_DEMO_MODE=true 一鍵切換,完全不需要修改 router 程式碼。


需要做的事

1. 新增 Service 抽象層(ABC Interface)

建立 app/services/ 目錄,為四個模組各建立 ABC interface,所有 method 必須與現有 router 的實際邏輯吻合:

app/services/__init__.py

空白或只有 __all__ 的 init 檔。

app/services/wifi_service.py

from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional

class WiFiService(ABC):

    @abstractmethod
    async def get_interface_details(self) -> Dict[str, Any]: ...

    @abstractmethod
    async def get_interface_list(self) -> Dict[str, Any]: ...

    @abstractmethod
    async def set_monitor_mode(self, interface: str) -> Dict[str, Any]: ...

    @abstractmethod
    async def get_interface_status(self, interface: str) -> Dict[str, Any]: ...

    @abstractmethod
    async def scan_networks(self, interface: str, timeout: int) -> Dict[str, Any]: ...

    @abstractmethod
    async def set_channel(self, interface: str, channel: str) -> Dict[str, Any]: ...

    @abstractmethod
    async def start_capture(self, interface: str, bssid: str, channel: int) -> Dict[str, Any]: ...

    @abstractmethod
    async def stop_capture(self) -> Dict[str, Any]: ...

    @abstractmethod
    async def send_deauth(self, interface: str, bssid: str, packets: int) -> Dict[str, Any]: ...

    @abstractmethod
    async def check_handshake(self, capture_file: str) -> Dict[str, Any]: ...

    @abstractmethod
    async def crack_password(self, capture_file: str, wordlist_file: str) -> Dict[str, Any]: ...

    @abstractmethod
    async def generate_wordlist(self, output_filename: str, info_data: Dict[str, List[str]]) -> Dict[str, Any]: ...

    @abstractmethod
    async def list_wordlists(self) -> Dict[str, Any]: ...

    @abstractmethod
    async def delete_wordlist(self, filename: str) -> Dict[str, Any]: ...

    @abstractmethod
    async def defense_scan(self, interface: str, timeout: int) -> Dict[str, Any]: ...

app/services/ble_service.py

from abc import ABC, abstractmethod
from typing import Any, Dict, List

class BLEService(ABC):

    @abstractmethod
    def get_profiles(self) -> List[Dict]: ...

    @abstractmethod
    async def add_profile(self, profile: Dict) -> Dict[str, Any]: ...

    @abstractmethod
    async def delete_profile(self, name: str) -> Dict[str, Any]: ...

    @abstractmethod
    async def start_beacon_emulator(self, profile_name: str) -> Dict[str, Any]: ...

    @abstractmethod
    async def stop_beacon_emulator(self) -> Dict[str, Any]: ...

    @abstractmethod
    async def get_beacon_emulator_status(self) -> Dict[str, Any]: ...

    @abstractmethod
    async def start_airpods_emulator(self) -> Dict[str, Any]: ...

    @abstractmethod
    async def stop_airpods_emulator(self) -> Dict[str, Any]: ...

    @abstractmethod
    async def get_airpods_emulator_status(self) -> Dict[str, Any]: ...

    @abstractmethod
    async def get_airpods_logs(self) -> Dict[str, Any]: ...

app/services/ir_service.py

from abc import ABC, abstractmethod
from typing import Any, Dict, Optional

class IRService(ABC):

    @abstractmethod
    async def start_recording(self) -> Dict[str, Any]: ...

    @abstractmethod
    async def cancel_recording(self) -> Dict[str, Any]: ...

    @abstractmethod
    async def get_status(self) -> Dict[str, Any]: ...

    @abstractmethod
    async def transmit(self, signal_data: str, format: Optional[str]) -> Dict[str, Any]: ...

    @abstractmethod
    async def enumerate_code(self, device_type: str, brand: str, protocol: str, function: str, code_index: int) -> Dict[str, Any]: ...

app/services/rfid_service.py

from abc import ABC, abstractmethod
from typing import Any, Dict, List

class RFIDService(ABC):

    @abstractmethod
    async def setup_pn532(self) -> Dict[str, Any]: ...

    @abstractmethod
    async def identify_rfid(self) -> Dict[str, Any]: ...

    @abstractmethod
    async def write_uid(self, card_data: Dict, save_to_db: bool) -> Dict[str, Any]: ...

    @abstractmethod
    async def analyze_rfid(self, card_info: Dict) -> Dict[str, Any]: ...

2. 新增 Real Service 實作

將現有 router 裡的硬體邏輯搬移到 real service,router 只呼叫 service。

app/services/real/__init__.py

空白。

app/services/real/wifi_real.py

app/api/WiFi.py 中所有非 HTML page route 的邏輯搬到這裡實作 WiFiService。原本 WiFi.py 裡的全域狀態變數 (ap_running, capture_active, capture_process, network_adapters 等) 也移至此 class 的 instance 變數。helper function (extract_adapter_names, parse_aircrack_output, run_capture_process) 也搬到這裡。

重要:start_capture 的 background task 在 real service 中需要特別處理。可以保留 BackgroundTasks 的使用,在 router 層傳入,或改用 asyncio.create_task。建議在 router 層傳遞 background_tasks,real service 的 start_capture 接收 background_tasks: BackgroundTasks 參數。

`app/services/real/ble_real.py...

This pull request was created from Copilot chat.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits February 23, 2026 07:23
…d demo mode via env var

Co-authored-by: 1PingSun <38580658+1PingSun@users.noreply.github.com>
Co-authored-by: 1PingSun <38580658+1PingSun@users.noreply.github.com>
Copilot AI changed the title [WIP] Add Demo mode for WebUI with FastAPI Add Demo Mode via Service Layer with FastAPI Dependency Injection Feb 23, 2026
Copilot AI requested a review from 1PingSun February 23, 2026 07:24
Copilot AI and others added 3 commits February 23, 2026 15:37
… service layer

Co-authored-by: 1PingSun <38580658+1PingSun@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants