# Deploy Operations 平台抽象重構 Implementation Plan

## Overview

實施完整的平台抽象重構，讓 `deploy_operations` 支援多種部署類型（Network Deployment 和 Direct Flashing），為未來支援 STM32、ESP32 等 MCU 平台打好基礎。

**核心原則**：
- 🎯 **KISS**：追求最簡潔的架構，不考慮向後兼容性
- 🔧 **工具無關**：deploy_operations 不知道底層使用 SSH、ST-LINK 還是 esptool
- 🚀 **易於擴展**：新增平台只需新增 template 中的 deploy.py

## Current State Analysis

### 已完成的工作（Commit eecfda7）

✅ **Maketool Abstraction Layer 基礎架構**：
- `deploy_operations.deploy_firmware()` 已經調用 `deploy.py` script
- 移除了 4 個平台相關函數（257 行代碼，-52.8%）
- deploy.py 支援遠端日誌參數
- 架構已經符合「編排層調用執行層」的模式

### 當前限制

❌ **問題 1：DeployConfig 是 SSH-specific**
```python
@dataclass
class DeployConfig:  # ← 名稱不明確
    board_ip: str          # ❌ STM32 不需要
    board_user: str        # ❌ STM32 不需要
    ssh_options: str       # ❌ STM32 不需要
    # ...
```

❌ **問題 2：缺少 Deploy 類型分流**
```python
# 目前 deploy_firmware() 直接調用 deploy.py
# 未來需要根據 sdk_type 選擇不同的處理流程
def deploy_firmware(build_dir: Path, config: DeployConfig) -> DeployResult:
    # ❌ 假設所有平台都使用 SSH
    deploy_script = build_dir.parent / ".fwauto" / "deploy" / "deploy.py"
    cmd = ["python3", str(deploy_script), "--board-ip", config.board_ip, ...]
```

❌ **問題 3：缺少環境檢測**
- 目前沒有任何環境檢測（網路連線、SSH 可用性）
- 依賴 deploy.py 內部檢測（但無法跨平台共用）

### 關鍵發現

**從研究文件得到的洞察**：
- Network Deployment（SSH）和 Direct Flashing（JTAG/USB）是兩種完全不同的部署方式
- 所有 SSH-based 平台（AM62X、Raspberry Pi、RISC-V）可以共用 `_deploy_network()` 函數
- 所有 MCU 平台（STM32、ESP32、nRF52）可以共用 `_deploy_direct_flashing()` 函數
- 環境檢測（如 SSH port 檢查）可以在 FWAuto 層共用

**檔案位置**：
- `src/fwauto/deploy_operations.py` - 需要重構
- `src/fwauto/nodes/deploy.py` - 需要更新匯入
- `tests/nodes/test_deploy_makefile.py` - 需要更新（目前已 skip）

## Desired End State

### 目標架構

**Deploy 類型分流**：
```mermaid
flowchart TD
    DN["deploy_node"]
    DF["deploy_firmware()<br/>(路由函數)"]
    GDT["_get_deploy_type()"]

    subgraph "Network Deployment"
        DPN["_deploy_network()"]
        CNC["_check_network_connectivity()"]
        DPS_SSH["deploy.py<br/>(SSH)"]
    end

    subgraph "Direct Flashing"
        DPF["_deploy_direct_flashing()"]
        DPS_STM32["deploy.py<br/>(ST-LINK)"]
    end

    DN -->|"ProjectConfig<br/>binary_args"| DF
    DF --> GDT
    GDT -->|"network"| DPN
    GDT -->|"direct_flashing"| DPF

    DPN --> CNC
    CNC --> DPS_SSH
    DPF --> DPS_STM32

    style DF fill:#e3f2fd
    style DPN fill:#c8e6c9
    style DPF fill:#fff9c4
```

**SDK Type → Deploy Type 映射**：
```python
SDK_DEPLOY_TYPE_MAP = {
    # Network Deployment (SSH-based)
    "am62x": "network",
    "raspberry-pi": "network",
    "risc-v": "network",

    # Direct Flashing (MCU-based)
    "stm32": "direct_flashing",
    "esp32": "direct_flashing",
    "nrf52": "direct_flashing",
}
```

### 驗證方式

**自動化驗證**：
```bash
# 語法和類型檢查
make -C src/fwauto check

# 測試執行
pytest tests/nodes/test_deploy_makefile.py -v

# AM62X 部署測試（需要實體開發板）
fwauto deploy --binary-args "test"
```

**手動驗證**：
- AM62X 開發板能正常部署
- 遠端日誌功能正常
- Background mode 正常運作
- 錯誤訊息清晰（如 SSH 無法連線）

## What We're NOT Doing

遵循 KISS 原則，我們明確**不做**以下事項：

### 不考慮向後兼容性
- ❌ 不保留 `DeployConfig` 舊名稱 → 直接改為 `NetworkDeployConfig`
- ❌ 不保留 `ConnectivityError` exception → 直接刪除（已不使用）
- ❌ 不提供 migration guide → 產品還在開發階段

### 不提前設計未來功能
- ❌ 不定義 `DirectFlashingConfig` dataclass → 等真正需要 STM32 時再加
- ❌ 不實作完整的 `_deploy_direct_flashing()` → 只放 `raise NotImplementedError`
- ❌ 不設計複雜的 Config 繼承體系 → 簡單的 dataclass 即可

### 不做複雜的環境檢測
- ❌ 不做 ping 檢測 → 跨平台問題多
- ❌ 不檢查磁碟空間、防火牆等 → 留給 deploy.py
- ✅ 只檢查 SSH port (22) 是否可連線 → 簡單且有效

### 不支援 Production 環境
- ❌ 不支援 OTA Update
- ❌ 不支援 Bootloader Update (IAP/DFU)
- ❌ 不支援 SD Card OS 燒錄
- ✅ FWAuto 專注開發階段的快速迭代

## Implementation Approach

**分階段漸進式重構**：

1. **Phase 1**：建立分流架構骨架（不影響現有功能）
2. **Phase 2**：新增簡單的環境檢測
3. **Phase 3**：更新 deploy_node 使用新架構
4. **Phase 4**：文檔化與清理

**測試策略**：
- 每個 Phase 完成後執行自動化測試
- Phase 3 需要實體 AM62X 開發板手動測試

---

## Phase 1: 實作 Deploy 類型分流架構

### Overview

建立多平台支援的分流架構，將現有的 SSH-based 邏輯封裝到 `_deploy_network()` 函數中。

### 架構變更

**Before**：
```python
def deploy_firmware(build_dir: Path, config: DeployConfig) -> DeployResult:
    # 直接調用 deploy.py（假設是 SSH）
    deploy_script = build_dir.parent / ".fwauto" / "deploy" / "deploy.py"
    cmd = ["python3", str(deploy_script), "--board-ip", ...]
    result = subprocess.run(cmd, ...)
```

**After**：
```python
def deploy_firmware(build_dir: Path, config: ProjectConfig, binary_args: str) -> DeployResult:
    """路由函數：根據 sdk_type 分流到不同的部署方式"""
    deploy_type = _get_deploy_type(config.sdk_type)

    if deploy_type == "network":
        return _deploy_network(build_dir, config, binary_args)
    elif deploy_type == "direct_flashing":
        return _deploy_direct_flashing(build_dir, config, binary_args)
    else:
        raise ValueError(f"Unknown deploy type: {deploy_type}")
```

### Changes Required

#### 1. 新增 SDK → Deploy Type 映射表

**File**: `src/fwauto/deploy_operations.py`

**在檔案頂部（import 之後）新增**：
```python
# ============================================
# Deploy Type Mapping
# ============================================

SDK_DEPLOY_TYPE_MAP = {
    # Network Deployment (SSH-based, Linux SBC)
    "am62x": "network",
    "raspberry-pi": "network",
    "risc-v": "network",

    # Direct Flashing (MCU-based)
    "stm32": "direct_flashing",
    "esp32": "direct_flashing",
    "nrf52": "direct_flashing",
}


def _get_deploy_type(sdk_type: str | None) -> str:
    """
    Get deploy type from SDK type.

    Args:
        sdk_type: SDK type from ProjectConfig (e.g., "am62x", "stm32")

    Returns:
        Deploy type: "network" or "direct_flashing"

    Raises:
        ValueError: If sdk_type is None or unknown
    """
    if sdk_type is None:
        raise ValueError("sdk_type not set in config.toml")

    deploy_type = SDK_DEPLOY_TYPE_MAP.get(sdk_type)
    if deploy_type is None:
        raise ValueError(f"Unknown SDK type: {sdk_type}")

    return deploy_type
```

#### 2. 重新命名 DeployConfig → NetworkDeployConfig

**File**: `src/fwauto/deploy_operations.py`

**修改 dataclass 定義**：
```python
@dataclass
class NetworkDeployConfig:
    """Configuration for SSH-based network deployment (AM62X, Raspberry Pi, RISC-V)."""

    board_ip: str
    board_user: str = "root"
    deploy_path: str = "/home/root"
    ssh_options: str = "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5"
    binary_args: str = ""
    execution_mode: Literal["foreground", "background"] = "foreground"
    remote_log_enabled: bool = False
    remote_log_path: str = "/dev/null"
```

#### 3. 移除 ConnectivityError

**File**: `src/fwauto/deploy_operations.py`

**刪除整個 exception 定義**（第 61-69 行）：
```python
# ❌ 刪除這段
class ConnectivityError(Exception):
    """
    Raised when network connectivity check fails.
    ...
    """
    pass
```

#### 4. 重構 deploy_firmware() 為路由函數

**File**: `src/fwauto/deploy_operations.py`

**替換整個 `deploy_firmware()` 函數**：
```python
def deploy_firmware(build_dir: Path, config: "ProjectConfig", binary_args: str = "") -> DeployResult:
    """
    Deploy firmware by routing to appropriate deploy method based on SDK type.

    This function follows the platform abstraction pattern:
    - Determines deploy type from SDK type (network vs direct_flashing)
    - Routes to appropriate platform-specific deploy function
    - Platform-specific logic is delegated to deploy scripts

    Args:
        build_dir: Path to build directory containing the binary
        config: Project configuration (contains sdk_type and deployment settings)
        binary_args: Arguments to pass to the binary (optional)

    Returns:
        DeployResult indicating success/failure

    Raises:
        ValueError: If sdk_type is unknown or not set
    """
    from .config import ProjectConfig  # Import here to avoid circular dependency

    logger.info("=" * 60)
    logger.info("FWAuto Deploy: Platform-Agnostic Deployment")
    logger.info("=" * 60)

    # Determine deploy type from SDK type
    deploy_type = _get_deploy_type(config.sdk_type)
    logger.info(f"📦 SDK Type: {config.sdk_type}")
    logger.info(f"🚀 Deploy Type: {deploy_type}")
    logger.info("")

    # Route to appropriate deploy method
    if deploy_type == "network":
        return _deploy_network(build_dir, config, binary_args)
    elif deploy_type == "direct_flashing":
        return _deploy_direct_flashing(build_dir, config, binary_args)
    else:
        # Should never reach here due to _get_deploy_type validation
        raise ValueError(f"Unknown deploy type: {deploy_type}")
```

#### 5. 實作 _deploy_network() 函數

**File**: `src/fwauto/deploy_operations.py`

**新增函數（將現有 deploy_firmware 的邏輯移過來）**：
```python
def _deploy_network(build_dir: Path, config: "ProjectConfig", binary_args: str) -> DeployResult:
    """
    Deploy to Linux-based platforms via SSH/SCP (AM62X, Raspberry Pi, RISC-V).

    Process:
    1. Build NetworkDeployConfig from ProjectConfig
    2. Locate deploy.py script
    3. Build command with SSH-specific parameters
    4. Execute deploy.py via subprocess
    5. Parse result and return DeployResult

    Args:
        build_dir: Path to build directory
        config: Project configuration
        binary_args: Arguments to pass to the binary

    Returns:
        DeployResult indicating success/failure
    """
    logger.info("📡 Network Deployment (SSH-based)")
    logger.info("")

    # Build NetworkDeployConfig from ProjectConfig
    deploy_config = NetworkDeployConfig(
        board_ip=config.board_ip,
        board_user=config.board_user,
        deploy_path=config.deploy_path,
        ssh_options=config.ssh_options,
        binary_args=binary_args,
        execution_mode="background",  # Default to background
        remote_log_enabled=config.remote_log_enabled,
        remote_log_path="/dev/null",  # Will be set by deploy_node if needed
    )

    # Locate deploy.py script
    deploy_script = build_dir.parent / ".fwauto" / "deploy" / "deploy.py"

    if not deploy_script.exists():
        error_msg = f"Deploy script not found: {deploy_script}"
        logger.error(f"❌ {error_msg}")
        return DeployResult(success=False, message=error_msg)

    # Build deploy.py command (SSH-specific parameters)
    cmd = [
        "python3",
        str(deploy_script),
        "--board-ip", deploy_config.board_ip,
        "--board-user", deploy_config.board_user,
        "--deploy-path", deploy_config.deploy_path,
        "--ssh-options", deploy_config.ssh_options,
        "--execution-mode", deploy_config.execution_mode,
    ]

    # Add optional parameters
    if deploy_config.binary_args:
        cmd.extend(["--binary-args", deploy_config.binary_args])

    if deploy_config.remote_log_enabled and deploy_config.remote_log_path != "/dev/null":
        cmd.extend(["--remote-log-path", deploy_config.remote_log_path])

    # Log the command for debugging
    logger.info("💡 Invoking deploy script:")
    logger.info(f"   {' '.join(cmd)}")
    logger.info("")

    # Execute deploy.py
    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=120,  # 2 minutes timeout
            cwd=str(build_dir.parent),
        )

        # Parse result
        if result.returncode == 0:
            # Success
            logger.info("✅ Deploy completed successfully")
            logger.info("")
            logger.info("=" * 60)

            # Extract binary name from stderr output (deploy.py logs to stderr)
            binary_name = _extract_binary_name_from_output(result.stderr)

            # Extract PID if background mode
            pid = _extract_pid_from_output(result.stderr) if deploy_config.execution_mode == "background" else None

            return DeployResult(
                success=True,
                message=result.stderr.strip(),
                binary_name=binary_name,
                pid=pid,
            )
        else:
            # Failure
            error_msg = f"Deploy script failed with exit code {result.returncode}"
            logger.error(f"❌ {error_msg}")
            logger.error("Deploy script output:")
            logger.error(result.stderr)

            return DeployResult(
                success=False,
                message=result.stderr.strip() if result.stderr else error_msg,
            )

    except subprocess.TimeoutExpired:
        error_msg = "Deploy script timeout (120s)"
        logger.error(f"❌ {error_msg}")
        return DeployResult(success=False, message=error_msg)

    except Exception as e:
        error_msg = f"Failed to execute deploy script: {str(e)}"
        logger.error(f"❌ {error_msg}")
        return DeployResult(success=False, message=error_msg)
```

#### 6. 實作 _deploy_direct_flashing() 骨架

**File**: `src/fwauto/deploy_operations.py`

**新增函數（僅骨架，未來擴展用）**：
```python
def _deploy_direct_flashing(build_dir: Path, config: "ProjectConfig", binary_args: str) -> DeployResult:
    """
    Deploy to MCU platforms via direct flashing (STM32, ESP32, nRF52).

    This function is a placeholder for future MCU platform support.
    When implementing STM32 support, this function will:
    1. Locate platform-specific deploy.py (e.g., .fwauto/deploy/deploy_stm32.py)
    2. Build command with MCU-specific parameters (e.g., --hex-file, --stlink-cli)
    3. Execute deploy script via subprocess
    4. Parse result and return DeployResult

    Args:
        build_dir: Path to build directory
        config: Project configuration
        binary_args: Arguments to pass (may not be applicable for MCU)

    Returns:
        DeployResult indicating success/failure

    Raises:
        NotImplementedError: Direct flashing not yet implemented
    """
    logger.error("❌ Direct flashing support not yet implemented")
    logger.error(f"   SDK Type: {config.sdk_type}")
    logger.error(f"   Supported platforms: STM32, ESP32, nRF52 (future)")
    logger.error("")
    logger.error("To add STM32 support:")
    logger.error("1. Create .fwauto/deploy/deploy.py for STM32 platform")
    logger.error("2. Implement this function to call the deploy script")
    logger.error("3. Update tests")

    raise NotImplementedError(
        f"Direct flashing not yet implemented for SDK type: {config.sdk_type}"
    )
```

### Success Criteria

#### Automated Verification:

- [ ] Python 語法檢查通過：`python3 -m py_compile src/fwauto/deploy_operations.py`
- [ ] 型別檢查通過：`pyright src/fwauto/deploy_operations.py`
- [ ] Lint 檢查通過：`ruff check src/fwauto/deploy_operations.py`
- [ ] 新增的函數存在：
  - `_get_deploy_type()` 存在
  - `_deploy_network()` 存在
  - `_deploy_direct_flashing()` 存在
- [ ] `NetworkDeployConfig` dataclass 存在
- [ ] `ConnectivityError` 已移除
- [ ] `SDK_DEPLOY_TYPE_MAP` 常數存在

#### Manual Verification:

- [ ] 檔案可正常匯入：`python3 -c "from fwauto.deploy_operations import NetworkDeployConfig, deploy_firmware"`
- [ ] `_get_deploy_type("am62x")` 回傳 `"network"`
- [ ] `_get_deploy_type("stm32")` 回傳 `"direct_flashing"`
- [ ] `_get_deploy_type("unknown")` 拋出 `ValueError`
- [ ] `_deploy_direct_flashing()` 呼叫時拋出 `NotImplementedError`

**Implementation Note**: Phase 1 完成後，現有的 AM62X 部署功能**不應受影響**（因為 deploy_node 尚未更新）。

---

## Phase 2: 新增簡單的環境檢測

### Overview

在 `_deploy_network()` 開頭加入簡單的 SSH port 檢測，提早發現網路連線問題。

### Changes Required

#### 1. 實作 _check_network_connectivity()

**File**: `src/fwauto/deploy_operations.py`

**在 `_deploy_network()` 之前新增**：
```python
def _check_network_connectivity(board_ip: str, port: int = 22, timeout: int = 5) -> None:
    """
    Check if SSH port is reachable (simple connectivity check).

    This is a lightweight check to fail fast if the board is not reachable.
    More comprehensive checks (authentication, disk space, etc.) are done by deploy.py.

    Args:
        board_ip: Board IP address
        port: SSH port (default: 22)
        timeout: Connection timeout in seconds (default: 5)

    Raises:
        ConnectionError: If SSH port is not reachable
    """
    import socket

    logger.debug(f"🔍 Checking SSH connectivity to {board_ip}:{port}")

    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        result = sock.connect_ex((board_ip, port))
        sock.close()

        if result == 0:
            logger.debug(f"✅ SSH port {port} is reachable")
        else:
            raise ConnectionError(
                f"SSH port {port} is not reachable on {board_ip}. "
                f"Please check:\n"
                f"  - Board is powered on and connected to network\n"
                f"  - IP address is correct ({board_ip})\n"
                f"  - SSH service is running on the board"
            )
    except socket.timeout:
        raise ConnectionError(
            f"Connection timeout when checking SSH port on {board_ip}:{port}. "
            f"Please check network connectivity."
        )
    except Exception as e:
        raise ConnectionError(f"Failed to check network connectivity: {str(e)}")
```

#### 2. 整合到 _deploy_network()

**File**: `src/fwauto/deploy_operations.py`

**在 `_deploy_network()` 函數開頭（建立 NetworkDeployConfig 之後）加入**：
```python
def _deploy_network(build_dir: Path, config: "ProjectConfig", binary_args: str) -> DeployResult:
    """..."""
    logger.info("📡 Network Deployment (SSH-based)")
    logger.info("")

    # Build NetworkDeployConfig from ProjectConfig
    deploy_config = NetworkDeployConfig(...)

    # ✅ 新增：環境檢測
    try:
        _check_network_connectivity(deploy_config.board_ip)
    except ConnectionError as e:
        error_msg = f"Network connectivity check failed: {str(e)}"
        logger.error(f"❌ {error_msg}")
        return DeployResult(success=False, message=error_msg)

    # 原有的邏輯繼續...
    deploy_script = build_dir.parent / ".fwauto" / "deploy" / "deploy.py"
    # ...
```

### Success Criteria

#### Automated Verification:

- [ ] Python 語法檢查通過：`python3 -m py_compile src/fwauto/deploy_operations.py`
- [ ] 型別檢查通過：`pyright src/fwauto/deploy_operations.py`
- [ ] `_check_network_connectivity()` 函數存在

#### Manual Verification:

- [ ] 測試成功案例：`python3 -c "from fwauto.deploy_operations import _check_network_connectivity; _check_network_connectivity('192.168.50.169')"`（需要開發板在線）
- [ ] 測試失敗案例：`python3 -c "from fwauto.deploy_operations import _check_network_connectivity; _check_network_connectivity('192.168.50.254')"` 應拋出 `ConnectionError`
- [ ] 錯誤訊息包含故障排除建議（IP、網路、SSH 服務）

---

## Phase 3: 更新 deploy_node 使用新架構

### Overview

更新 `deploy_node` 使用新的 API（`NetworkDeployConfig` 和 `deploy_firmware` 新簽名）。

### Changes Required

#### 1. 更新 import 和類型

**File**: `src/fwauto/nodes/deploy.py`

**修改 import 語句**（第 7 行）：
```python
# Before
from ..deploy_operations import ConnectivityError, DeployConfig, deploy_firmware

# After
from ..deploy_operations import NetworkDeployConfig, deploy_firmware
```

**移除 ConnectivityError 的 except 子句**（第 190-198 行）：
```python
# ❌ 刪除這段
    except ConnectivityError as e:
        error_msg = f"Network connectivity error: {str(e)}"
        log_node_error("deploy", error_msg)
        return {
            **state,
            "deploy_status": "error",
            "deploy_errors": [str(e)],
            "deploy_log": f"❌ {error_msg}",
        }
```

#### 2. 更新 deploy_firmware 調用

**File**: `src/fwauto/nodes/deploy.py`

**修改 deploy_firmware 調用**（第 143-156 行）：
```python
# Before
deploy_config = DeployConfig(
    board_ip=config.board_ip,
    board_user=config.board_user,
    deploy_path=config.deploy_path,
    ssh_options=config.ssh_options,
    binary_args=binary_args,
    execution_mode="background",
    remote_log_enabled=config.remote_log_enabled,
    remote_log_path=remote_log_path_local or "/dev/null",
)

result = deploy_firmware(build_dir=project_root_path / "build", config=deploy_config)

# After
# 移除 deploy_config 的建立，直接傳遞 ProjectConfig

result = deploy_firmware(
    build_dir=project_root_path / "build",
    config=config,  # ProjectConfig
    binary_args=binary_args,
)
```

#### 3. 處理遠端日誌路徑

**問題**：新的 `deploy_firmware()` 接受 `ProjectConfig`，但遠端日誌路徑需要在 `deploy_node` 中計算並傳遞給 `_deploy_network()`。

**解決方案**：修改 `_deploy_network()` 從 `config.deployment_config` 讀取 `remote_log_path`（如果有設定的話）。

**File**: `src/fwauto/nodes/deploy.py`

**在調用 deploy_firmware 之前設定 remote_log_path**：
```python
# Handle remote log configuration
remote_log_path_local = None
remote_log_path_full = None
if config.remote_log_enabled:
    try:
        from ..deploy_operations import find_binary as find_binary_op

        binary_path = find_binary_op(project_root_path / "build")
        binary_name = binary_path.name
        remote_log_path_local = _render_remote_log_path(config, binary_name)
        remote_log_path_full = f"{config.board_user}@{config.board_ip}:{remote_log_path_local}"

        logger.info(f"📊 Remote log will be saved to: {remote_log_path_full}")

        # ✅ 新增：將 remote_log_path 暫時寫入 config（透過 deployment_config）
        # 這樣 _deploy_network() 可以讀取到
        # Note: 這是一個臨時方案，更好的做法是讓 deploy_firmware 接受 remote_log_path 參數
        # 但為了 KISS，我們先用這個方式
        config.data.setdefault("deployment", {})
        config.data["deployment"].setdefault(config.sdk_type, {})
        config.data["deployment"][config.sdk_type]["_tmp_remote_log_path"] = remote_log_path_local

    except Exception as e:
        logger.warning(f"Failed to setup remote log: {e}")

# Execute deploy operation
result = deploy_firmware(
    build_dir=project_root_path / "build",
    config=config,
    binary_args=binary_args,
)
```

**File**: `src/fwauto/deploy_operations.py`

**修改 `_deploy_network()` 讀取遠端日誌路徑**：
```python
def _deploy_network(build_dir: Path, config: "ProjectConfig", binary_args: str) -> DeployResult:
    """..."""

    # 讀取遠端日誌路徑（如果 deploy_node 有設定）
    remote_log_path = config.deployment_config.get("_tmp_remote_log_path", "/dev/null")

    # Build NetworkDeployConfig from ProjectConfig
    deploy_config = NetworkDeployConfig(
        board_ip=config.board_ip,
        board_user=config.board_user,
        deploy_path=config.deploy_path,
        ssh_options=config.ssh_options,
        binary_args=binary_args,
        execution_mode="background",
        remote_log_enabled=config.remote_log_enabled,
        remote_log_path=remote_log_path,  # 使用從 deploy_node 傳來的值
    )

    # ... 其餘邏輯不變
```

### Success Criteria

#### Automated Verification:

- [ ] Python 語法檢查通過：`python3 -m py_compile src/fwauto/nodes/deploy.py`
- [ ] 型別檢查通過：`pyright src/fwauto/nodes/deploy.py`
- [ ] Lint 檢查通過：`ruff check src/fwauto/nodes/deploy.py`
- [ ] 所有單元測試通過（如果有）：`pytest tests/nodes/test_deploy*.py -v`

#### Manual Verification:

- [ ] AM62X 開發板能正常部署：`fwauto deploy`
- [ ] Binary args 正常傳遞：`fwauto deploy --binary-args "test"`
- [ ] Background mode 正常運作（可看到 PID）
- [ ] 遠端日誌功能正常：檢查 `.fwauto/config.toml` 中的 `last_log_file_path`
- [ ] 網路連線失敗時有清楚的錯誤訊息：關閉開發板後執行 `fwauto deploy`
- [ ] SSH port 無法連線時有故障排除建議

**Implementation Note**: 完成 Phase 3 後，需要在實體 AM62X 開發板上進行完整的手動測試，確保所有功能正常運作後才能繼續到 Phase 4。

---

## Phase 4: 文檔化與清理

### Overview

更新文檔說明新架構，並清理不必要的代碼。

### Changes Required

#### 1. 更新 deploy_operations.py 頂部 docstring

**File**: `src/fwauto/deploy_operations.py`

**替換頂部 module docstring**（第 1-15 行）：
```python
"""
Deploy operations module - Platform-agnostic deployment orchestration.

This module implements the platform abstraction pattern for firmware deployment:
- Supports multiple deploy types: Network Deployment (SSH) and Direct Flashing (JTAG/USB)
- Routes to platform-specific deploy scripts based on SDK type
- Platform-independent orchestration layer (no direct tool execution)

Supported platforms:
- Network Deployment: AM62X, Raspberry Pi, RISC-V SBC (via SSH/SCP)
- Direct Flashing: STM32, ESP32, nRF52 (future, via JTAG/USB) - not yet implemented

Architecture:
1. deploy_firmware() - Routes based on SDK type
2. _deploy_network() - SSH-based deployment with connectivity check
3. _deploy_direct_flashing() - JTAG/USB deployment (placeholder)
4. Platform-specific deploy.py - Actual deployment execution

Design principles:
- Platform-independent (no direct SSH/SCP/ST-LINK execution in Python)
- Tool-agnostic (doesn't know about SSH, ST-LINK, esptool, etc.)
- Easy to extend (add new platform = add new deploy.py template)
"""
```

#### 2. 更新 CLAUDE.md

**File**: `CLAUDE.md`

**在 "Maketool Abstraction Layer" 章節後新增**：
```markdown
### Deploy Operations Platform Abstraction

deploy_operations 支援多種部署類型，遵循平台無關設計：

**架構**：
```
deploy_firmware()
  └─ _get_deploy_type(sdk_type)
      ├─ "network" → _deploy_network()
      │   ├─ _check_network_connectivity()
      │   └─ deploy.py (SSH/SCP)
      │
      └─ "direct_flashing" → _deploy_direct_flashing()
          └─ deploy.py (ST-LINK/esptool/nrfjprog)
```

**SDK Type → Deploy Type 映射**：
- Network Deployment: `am62x`, `raspberry-pi`, `risc-v`
- Direct Flashing: `stm32`, `esp32`, `nrf52` (未來支援)

**環境檢測**：
- Network Deployment: 簡單的 SSH port 檢查（可跨平台共用）
- Direct Flashing: 無檢測（由 deploy.py 處理）

**新增平台範例**（未來 STM32 支援）：
1. 在 `SDK_DEPLOY_TYPE_MAP` 新增 `"stm32": "direct_flashing"`
2. 建立 `.fwauto/deploy/deploy.py` for STM32（調用 ST-LINK CLI）
3. 實作 `_deploy_direct_flashing()` 函數
4. 更新測試
```

#### 3. 新增 docs/architecture/deploy-abstraction.md

**File**: `docs/architecture/deploy-abstraction.md`

**新增文件**：
```markdown
# Deploy Operations 平台抽象架構

## 概述

deploy_operations 模組實作平台無關的部署編排層，支援多種部署類型：
- Network Deployment（SSH-based）：適用 Linux SBC
- Direct Flashing（JTAG/USB-based）：適用 MCU 平台

## 架構圖

```mermaid
flowchart TD
    DN["deploy_node"]
    DF["deploy_firmware()<br/>(路由函數)"]

    subgraph "Network Deployment"
        DPN["_deploy_network()"]
        CNC["_check_network_connectivity()"]
        DPS_SSH["deploy.py (SSH)"]
    end

    subgraph "Direct Flashing"
        DPF["_deploy_direct_flashing()"]
        DPS_STM32["deploy.py (ST-LINK)"]
    end

    DN --> DF
    DF -->|"sdk_type: am62x"| DPN
    DF -->|"sdk_type: stm32"| DPF

    DPN --> CNC --> DPS_SSH
    DPF --> DPS_STM32
```

## 平台支援

### 已支援

| SDK Type | Deploy Type | 工具 | 狀態 |
|----------|------------|------|------|
| am62x | Network Deployment | SSH/SCP | ✅ 已支援 |

### 未來支援

| SDK Type | Deploy Type | 工具 | 預計實作 |
|----------|------------|------|---------|
| raspberry-pi | Network Deployment | SSH/SCP | 重用 AM62X 流程 |
| risc-v | Network Deployment | SSH/SCP | 重用 AM62X 流程 |
| stm32 | Direct Flashing | ST-LINK | 需實作 |
| esp32 | Direct Flashing | esptool | 需實作 |
| nrf52 | Direct Flashing | J-Link | 需實作 |

## 如何新增新平台

### 範例：新增 STM32 支援

1. **更新映射表**：
```python
# src/fwauto/deploy_operations.py
SDK_DEPLOY_TYPE_MAP = {
    # ...
    "stm32": "direct_flashing",  # ← 新增
}
```

2. **建立 deploy.py template**：
```bash
# src/fwauto/templates/sdk_stm32/deploy/deploy.py
# 實作 ST-LINK CLI 調用邏輯
```

3. **實作 _deploy_direct_flashing()**：
```python
def _deploy_direct_flashing(build_dir: Path, config: ProjectConfig, binary_args: str):
    deploy_script = build_dir.parent / ".fwauto" / "deploy" / "deploy.py"

    # 找到 hex/bin 檔案
    hex_file = find_hex_file(build_dir)

    # 調用 deploy.py（平台特定參數）
    cmd = ["python3", str(deploy_script), "--hex-file", str(hex_file)]
    result = subprocess.run(cmd, ...)

    return _parse_result(result)
```

4. **更新測試**：
```python
# tests/test_deploy_stm32.py
def test_stm32_direct_flashing():
    ...
```

## 設計原則

1. **工具無關**：deploy_operations 不知道 SSH、ST-LINK、esptool 的存在
2. **平台特定邏輯在 deploy.py**：所有工具調用都在 deploy.py 中
3. **環境檢測共用**：SSH 檢測可跨 Linux 平台共用
4. **KISS**：不提前設計未使用的功能
```

### Success Criteria

#### Automated Verification:

- [ ] 所有文檔檔案存在：
  - `CLAUDE.md` 包含 Deploy Operations Platform Abstraction 章節
  - `docs/architecture/deploy-abstraction.md` 存在
- [ ] Markdown 語法正確：`markdownlint CLAUDE.md docs/architecture/deploy-abstraction.md`

#### Manual Verification:

- [ ] 文檔內容正確且易讀
- [ ] Mermaid 圖表可正常渲染
- [ ] 「如何新增新平台」範例清楚易懂
- [ ] 所有檔案路徑和程式碼範例正確

---

## Testing Strategy

### Unit Tests

**目前狀況**：
- `tests/nodes/test_deploy_makefile.py` 已全部 skip（因為不再使用 Makefile）
- 沒有 `test_deploy_operations.py`

**建議**（可選，不在此計劃範圍）：
- 未來可新增 `tests/test_deploy_operations.py` 測試分流邏輯
- 測試 `_get_deploy_type()` 函數
- Mock ProjectConfig 測試 `_deploy_network()`

### Integration Tests

**手動測試**（Phase 3 完成後）：
1. 正常部署：`fwauto deploy`
2. 帶參數部署：`fwauto deploy --binary-args "test"`
3. 網路斷線測試：關閉開發板，確認錯誤訊息清楚
4. 遠端日誌測試：確認 `last_log_file_path` 正確寫入 config
5. Background mode 測試：確認可看到 PID

### Regression Tests

確保不影響現有功能：
- [ ] `fwauto build` 正常
- [ ] `fwauto run` 正常（build + deploy）
- [ ] `fwauto log "有任何 error 嗎?"` 正常

---

## Performance Considerations

**預期影響**：
- 新增 `_check_network_connectivity()` 會增加約 1-5 秒（依網路速度）
- 函數調用層級增加（`deploy_firmware` → `_deploy_network`），但影響可忽略（< 1ms）

**優化空間**：
- 可考慮 cache `_get_deploy_type()` 結果（但目前不需要，KISS）
- 可並行執行環境檢測和 script 定位（但目前不需要，KISS）

---

## Migration Notes

**不適用**：本重構不需要 migration，因為：
- 產品還在開發階段
- 沒有外部 API 使用者
- 直接修改即可，不考慮向後兼容性

---

## References

- **研究文件**: `.ai/prpi/research/2025-11-12-deploy-operations-abstraction-analysis.md`
- **技術文件**: `docs/deploy-flows.md` - Deploy 流程類型說明
- **先前實作**: `.ai/prpi/implementations/2025-11-12-deploy-operations-abstraction-refactor.md` - Maketool Abstraction Layer 基礎
- **相關 Commit**: `eecfda7` - refactor(deploy): implement Maketool abstraction layer for deployment

---

**計劃版本**: 1.0.0
**建立日期**: 2025-11-13
**作者**: Claude (Sonnet 4.5) + Kent
**遵守規範**: ✅ KISS 原則 ✅ 深思熟慮 ✅ 計劃優先
