---
date: 2025-11-12
status: draft
tags: [plan, deploy, abstraction-layer, refactoring]
related_research: .ai/prpi/research/2025-11-12-deploy-operations-abstraction-analysis.md
---

# Deploy Operations Abstraction Layer 重構計劃

## Overview

本重構旨在讓 `deploy_operations` 模組遵循 CLAUDE.md 定義的 **Maketool Abstraction Layer** 原則，使其職責從「直接執行 SSH/SCP 工具」轉變為「資訊蒐集和 script 調用」，將所有平台相關邏輯委託給 `deploy.py` script。

**重構目標**：
- deploy_operations 只負責**平台無關**的資訊蒐集和參數構建
- deploy.py 負責**所有平台相關**的部署邏輯（網路檢查、SSH、SCP、錯誤診斷）
- 與 build_node 的架構保持一致

**設計原則**（來自研究文件）：
> deploy_operations 原本就應該儘量平台獨立，只負責蒐集 FWAuto 提供的信息，而不應該跟開發板有太多的關聯

## Current State Analysis

### 目前的問題

**架構違反抽象原則**：
```mermaid
flowchart TD
    DN[deploy_node]
    DO[deploy_operations]
    DP[deploy.py]
    B[Board]

    DN -->|DeployConfig| DO
    DO -->|"❌ 直接執行"| SSH[SSH subprocess]
    DO -->|"❌ 直接執行"| SCP[SCP subprocess]
    SSH --> B
    SCP --> B

    DP -.❌ 未被調用.-> B

    style DO fill:#fbb,stroke:#333
    style DP fill:#ddd,stroke:#333,stroke-dasharray: 5 5
```

**具體問題**：

1. **職責混淆** (src/fwauto/deploy_operations.py:365-423)：
   - `deploy_firmware()` 直接調用 `check_network_connectivity()`, `deploy_binary()`, `execute_on_board()`
   - Python 代碼明確知道 SSH/SCP 的存在

2. **功能重複**：
   - `deploy_operations.py` 實作完整部署邏輯（424 行）
   - `deploy.py` 也實作相同邏輯（419 行）
   - 兩者**完全獨立**，無代碼重用

3. **deploy.py 未被使用** (src/fwauto/deploy_operations.py:387-403)：
   - 構建了 deploy.py 命令，但僅記錄到 log
   - 實際執行仍然是 Python 函數

4. **平台相關診斷在錯誤位置** (src/fwauto/deploy_operations.py:68-126)：
   - `check_network_connectivity()` 包含詳細的錯誤診斷和故障排除建議
   - 這些診斷應該在 deploy.py 中，而非 deploy_operations

### 與 build_node 的對比

| 層面 | build_node (✅ 正確) | deploy_node (❌ 錯誤) |
|------|---------------------|---------------------|
| **Operations 職責** | 構建 make 命令 | 直接執行 SSH/SCP |
| **委託對象** | Makefile | Python 函數 |
| **抽象層級** | 高（不知道 Keil/GCC） | 低（知道 SSH/SCP） |
| **擴展方式** | 新增 Makefile | 修改 Python 代碼 |

**build_node 的正確模式** (src/fwauto/nodes/build.py:77-84)：
```python
from ..utils import execute_make_command

returncode, full_output = execute_make_command(
    makefile_path=makefile_path,
    target=target,
    project_root=project_root_path,
    timeout=60,
)
```

## Desired End State

### 目標架構

```mermaid
flowchart TD
    subgraph L1["Layer 1: Workflow Orchestration"]
        DN[deploy_node]
    end

    subgraph L2["Layer 2: Configuration & Invocation"]
        DO[deploy_operations]
    end

    subgraph L3["Layer 3: Tool Execution"]
        DP[deploy.py]
    end

    subgraph L4["Layer 4: Target System"]
        B[Board / Device]
    end

    DN -->|"FirmwareState<br/>DeployConfig"| DO
    DO -->|"✅ subprocess.run(['python3', 'deploy.py', ...])"| DP
    DP -->|"1. 網路檢查<br/>2. 尋找 binary<br/>3. SCP copy<br/>4. SSH execute"| B

    style L2 fill:#e3f2fd,stroke:#333
    style L3 fill:#e8f5e9,stroke:#333
```

### 職責劃分

**deploy_operations 的職責** (應該做的)：
- ✅ 從 FirmwareState 和 ProjectConfig 蒐集資訊
- ✅ 構建 deploy.py 的完整命令列
- ✅ 執行 `subprocess.run(["python3", "deploy.py", ...])`
- ✅ 解析 exit code 和基本輸出
- ✅ 返回 DeployResult

**deploy_operations 的職責** (不應該做的)：
- ❌ 平台相關的網路檢查、SSH 診斷
- ❌ 豐富的錯誤診斷和故障排除建議
- ❌ 直接執行 SSH/SCP 工具
- ❌ 知道底層使用什麼部署方式

**deploy.py 的職責**：
- ✅ 完整的部署流程（檢查 → 傳輸 → 執行）
- ✅ 所有平台相關邏輯
- ✅ 豐富的錯誤診斷和使用者訊息
- ✅ 可獨立執行（CLI + ENV 參數）

### 驗證方式

**成功標準**：
1. `deploy_operations.deploy_firmware()` 僅調用 deploy.py，不直接執行 SSH/SCP
2. deploy.py 支援 `--remote-log-path` 參數
3. 所有測試通過
4. 實際部署到 AM62X 開發板功能正常

## What We're NOT Doing

明確不在本次重構範圍內的項目：

1. **不修改 deploy_node.py**：
   - deploy_node 的邏輯保持不變
   - 遠端日誌渲染流程保持不變
   - 僅修改 deploy_operations 和 deploy.py

2. **不修改 deploy.py 的核心邏輯**：
   - 網路檢查、SCP、SSH 執行邏輯保持不變
   - 僅新增 `--remote-log-path` 參數支援

3. **不實作 STM32 部署**：
   - 本次僅處理 AM62X 的重構
   - STM32 deploy.py 未來獨立實作

4. **不修改 FirmwareState**：
   - State 欄位保持不變
   - 不新增任何 state 欄位

5. **不改變部署行為**：
   - 使用者體驗保持一致
   - 部署結果應該與重構前完全相同

## Implementation Approach

### 整體策略

採用**漸進式重構**，每個 Phase 獨立完成並可驗證：

1. **Phase 1**：擴展 deploy.py 支援遠端日誌參數
2. **Phase 2**：重構 deploy_operations 調用 deploy.py
3. **Phase 3**：清理不再需要的平台相關代碼

### 重構流程圖

```mermaid
flowchart LR
    P1["Phase 1<br/>擴展 deploy.py"]
    P2["Phase 2<br/>重構 deploy_operations"]
    P3["Phase 3<br/>清理舊代碼"]
    Done["✅ 完成"]

    P1 -->|"deploy.py 支援<br/>--remote-log-path"| P2
    P2 -->|"deploy_operations<br/>調用 deploy.py"| P3
    P3 -->|"移除重複代碼"| Done

    style P1 fill:#fff3e0,stroke:#333
    style P2 fill:#e3f2fd,stroke:#333
    style P3 fill:#e8f5e9,stroke:#333
```

### 風險管理

**風險點**：
1. deploy.py 的輸出格式變更可能影響解析
2. 遠端日誌功能可能失效

**緩解措施**：
1. 每個 Phase 都要執行完整測試
2. Phase 2 完成後進行實際開發板測試
3. 保留詳細的錯誤訊息記錄

---

## Phase 1: 新增 deploy.py 的遠端日誌支援

### Overview

擴展 `deploy.py` 支援 `--remote-log-path` CLI 參數，使其能夠接收 FWAuto 傳遞的遠端日誌路徑並寫入日誌。

**目標**：
- deploy.py 可透過 CLI 參數控制遠端日誌路徑
- 支援環境變數 `$REMOTE_LOG_PATH`
- background mode 使用參數指定的日誌路徑

### Changes Required

#### 1. deploy.py - 新增 CLI 參數

**檔案**：`src/fwauto/templates/sdk_am62x_linux/deploy/deploy.py`

**變更位置**：`get_config()` 函數（第 34-87 行）

**新增參數**：
```python
parser.add_argument(
    "--remote-log-path",
    default=os.getenv("REMOTE_LOG_PATH", ".fwauto/logs/app.log"),
    help="Remote log file path for background execution (default: .fwauto/logs/app.log or $REMOTE_LOG_PATH)",
)
```

**完整修改後的 get_config()**：
```python
def get_config():
    """Parse command line arguments and environment variables."""
    parser = argparse.ArgumentParser(
        description="AM62X Deploy: Deploy and execute binary via SSH",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )

    parser.add_argument(
        "--board-ip",
        default=os.getenv("BOARD_IP", "192.168.50.169"),
        help="Board IP address (default: %(default)s or $BOARD_IP)",
    )

    parser.add_argument(
        "--board-user",
        default=os.getenv("BOARD_USER", "root"),
        help="SSH user (default: %(default)s or $BOARD_USER)",
    )

    parser.add_argument(
        "--deploy-path",
        default=os.getenv("DEPLOY_PATH", "/home/root"),
        help="Remote deployment path (default: %(default)s or $DEPLOY_PATH)",
    )

    parser.add_argument(
        "--ssh-options",
        default=os.getenv(
            "SSH_OPTIONS", "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5"
        ),
        help="SSH options (default: %(default)s or $SSH_OPTIONS)",
    )

    parser.add_argument(
        "--binary-args",
        default=os.getenv("BINARY_ARGS", ""),
        help="Arguments to pass to binary (default: '' or $BINARY_ARGS)",
    )

    parser.add_argument(
        "--execution-mode",
        choices=["foreground", "background"],
        default=os.getenv("EXECUTION_MODE", "foreground"),
        help="Execution mode (default: %(default)s or $EXECUTION_MODE)",
    )

    parser.add_argument(
        "--build-dir",
        type=Path,
        default=Path(__file__).resolve().parents[2] / "build",
        help="Build directory containing binary (default: ../../build)",
    )

    # 新增：遠端日誌路徑參數
    parser.add_argument(
        "--remote-log-path",
        default=os.getenv("REMOTE_LOG_PATH", ".fwauto/logs/app.log"),
        help="Remote log file path for background execution (default: .fwauto/logs/app.log or $REMOTE_LOG_PATH)",
    )

    return parser.parse_args()
```

#### 2. deploy.py - 修改 build_remote_script() 使用日誌參數

**檔案**：`src/fwauto/templates/sdk_am62x_linux/deploy/deploy.py`

**變更位置**：`build_remote_script()` 函數（第 328-377 行）

**目前問題**：
- background mode 的日誌路徑寫死為 `/dev/null`（第 357 行）
- 無法自訂日誌路徑

**修改後的函數簽名**：
```python
def build_remote_script(binary_name, deploy_path, binary_args, execution_mode, remote_log_path=""):
    """Build the bash script to execute on remote board."""
    # Escape single quotes in arguments to prevent injection
    binary_args_escaped = binary_args.replace("'", "'\\''")

    if execution_mode == "foreground":
        return f"""
set -eo pipefail
cd '{deploy_path}' || {{ echo "ERROR: Cannot cd to {deploy_path}" >&2; exit 1; }}
pkill -9 '{binary_name}' 2>/dev/null || true
chmod +x './{binary_name}' || {{ echo "ERROR: Cannot chmod {binary_name}" >&2; exit 1; }}
echo "Executing ./{binary_name} {binary_args_escaped}..." >&2
'./{binary_name}' {binary_args_escaped}
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
    echo "SUCCESS: Binary executed successfully" >&2
    exit 0
else
    echo "ERROR: Binary exited with code $EXIT_CODE" >&2
    exit 1
fi
"""
    else:
        # Background mode with configurable logging
        log_path_escaped = remote_log_path.replace("'", "'\\''") if remote_log_path else ""

        if log_path_escaped:
            # 有指定日誌路徑：寫入日誌
            import time
            timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
            log_setup = f"""
mkdir -p "$(dirname '{log_path_escaped}')"
cat >> '{log_path_escaped}' <<'LOG_HEADER'

==========================================
Firmware Execution: {timestamp}
Binary: {binary_name}
Arguments: {binary_args_escaped}
==========================================

LOG_HEADER
LOG_TARGET='{log_path_escaped}'
"""
        else:
            # 無指定日誌路徑：使用 /dev/null
            log_setup = "LOG_TARGET='/dev/null'"

        return f"""
set -e
cd '{deploy_path}' || {{ echo "ERROR: Cannot cd to {deploy_path}" >&2; exit 1; }}
pkill -9 '{binary_name}' 2>/dev/null || true
chmod +x './{binary_name}' || {{ echo "ERROR: Cannot chmod {binary_name}" >&2; exit 1; }}
{log_setup}
nohup './{binary_name}' {binary_args_escaped} </dev/null >> "$LOG_TARGET" 2>&1 &
FIRMWARE_PID=$!
sleep 0.5

# Check if process is still running (daemon) or completed successfully (short-lived)
if kill -0 $FIRMWARE_PID 2>/dev/null; then
    echo "SUCCESS: Firmware started with PID $FIRMWARE_PID (running)" >&2
    exit 0
else
    # Process already exited, check if it was successful
    wait $FIRMWARE_PID 2>/dev/null
    EXIT_CODE=$?
    if [ $EXIT_CODE -eq 0 ]; then
        echo "SUCCESS: Firmware completed successfully" >&2
        exit 0
    else
        echo "ERROR: Firmware failed with exit code $EXIT_CODE" >&2
        exit 1
    fi
fi
"""
```

#### 3. deploy.py - 修改 execute_on_board() 調用

**檔案**：`src/fwauto/templates/sdk_am62x_linux/deploy/deploy.py`

**變更位置**：`execute_on_board()` 函數（第 274-325 行）

**修改函數簽名**：
```python
def execute_on_board(binary_name, board_ip, board_user, deploy_path, ssh_options, binary_args, execution_mode, remote_log_path=""):
```

**修改調用 build_remote_script 的位置**（第 305 行）：
```python
# Build remote execution script
remote_script = build_remote_script(binary_name, deploy_path, binary_args, execution_mode, remote_log_path)
```

#### 4. deploy.py - 修改 main() 傳遞參數

**檔案**：`src/fwauto/templates/sdk_am62x_linux/deploy/deploy.py`

**變更位置**：`main()` 函數（第 401-408 行）

**修改 execute_on_board 調用**：
```python
# Step 4: Execute on board
execute_on_board(
    binary_name,
    config.board_ip,
    config.board_user,
    config.deploy_path,
    config.ssh_options,
    config.binary_args,
    config.execution_mode,
    config.remote_log_path,  # 新增：傳遞遠端日誌路徑
)
```

### Success Criteria

#### Automated Verification

- [x] Python 語法檢查通過：`python3 -m py_compile src/fwauto/templates/sdk_am62x_linux/deploy/deploy.py`
- [x] deploy.py 可獨立執行：`python3 .fwauto/deploy/deploy.py --help`
- [x] 新參數出現在 help 訊息中：`python3 .fwauto/deploy/deploy.py --help | grep remote-log-path`

#### Manual Verification

- [ ] deploy.py 使用 `--remote-log-path /tmp/test.log` 參數執行成功
- [ ] 確認遠端開發板上 `/tmp/test.log` 檔案存在且包含日誌
- [ ] deploy.py 不指定 `--remote-log-path` 時預設使用 `.fwauto/logs/app.log`（相對於 deploy_path）
- [ ] 環境變數 `REMOTE_LOG_PATH=/tmp/env.log` 生效

**Implementation Note**: 完成此階段並通過所有 Automated Verification 後，暫停進行 Manual Verification 確認獨立 script 功能正常。確認後再進入 Phase 2。

---

## Phase 2: 重構 deploy_operations 調用 deploy.py

### Overview

重構 `deploy_operations.deploy_firmware()` 函數，將直接執行 SSH/SCP 的方式改為調用 `deploy.py` script，實現 Maketool Abstraction Layer 模式。

**目標**：
- deploy_operations 不再直接執行 SSH/SCP
- 透過 subprocess 調用 deploy.py
- 解析 deploy.py 的 exit code 和輸出
- 保持 deploy_node 的調用介面不變

### Architecture Change

**重構前**：
```python
def deploy_firmware(build_dir, config):
    check_network_connectivity(config.board_ip)  # ❌ 直接執行
    binary_path = find_binary(build_dir)
    binary_name = deploy_binary(binary_path, config)  # ❌ 直接執行
    result = execute_on_board(binary_name, config)  # ❌ 直接執行
    return result
```

**重構後**：
```python
def deploy_firmware(build_dir, config):
    deploy_script = build_dir.parent / ".fwauto" / "deploy" / "deploy.py"
    cmd = ["python3", str(deploy_script), "--board-ip", config.board_ip, ...]  # ✅ 構建命令
    result = subprocess.run(cmd, ...)  # ✅ 調用 script
    return _parse_deploy_result(result)  # ✅ 解析結果
```

### Changes Required

#### 1. deploy_operations.py - 重構 deploy_firmware() 函數

**檔案**：`src/fwauto/deploy_operations.py`

**變更位置**：`deploy_firmware()` 函數（第 365-423 行）

**完整重構後的函數**：
```python
def deploy_firmware(build_dir: Path, config: DeployConfig) -> DeployResult:
    """
    Complete deploy operation by invoking deploy.py script.

    This function follows the Maketool Abstraction Layer pattern:
    - Collects configuration from FirmwareState and ProjectConfig
    - Builds the deploy.py command line
    - Executes the script via subprocess
    - Parses the result and returns DeployResult

    Args:
        build_dir: Path to build directory containing the binary
        config: Deploy configuration

    Returns:
        DeployResult indicating success/failure
    """
    logger.info("=" * 60)
    logger.info("AM62X Deploy: Deploy and Execute")
    logger.info("=" * 60)

    # 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
    cmd = [
        "python3",
        str(deploy_script),
        "--board-ip", config.board_ip,
        "--board-user", config.board_user,
        "--deploy-path", config.deploy_path,
        "--ssh-options", config.ssh_options,
        "--execution-mode", config.execution_mode,
    ]

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

    if config.remote_log_enabled and config.remote_log_path != "/dev/null":
        cmd.extend(["--remote-log-path", 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 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)
```

#### 2. deploy_operations.py - 新增輔助函數

**檔案**：`src/fwauto/deploy_operations.py`

**新增位置**：在 `deploy_firmware()` 之前

**新增函數 1：提取 binary name**
```python
def _extract_binary_name_from_output(stderr: str) -> str:
    """
    Extract binary name from deploy.py stderr output.

    Args:
        stderr: stderr output from deploy.py

    Returns:
        Binary name if found, empty string otherwise
    """
    import re

    # deploy.py outputs: "ℹ️  Found binary: <name>"
    match = re.search(r"Found binary:\s+(\S+)", stderr)
    if match:
        return match.group(1)

    return ""
```

**新增函數 2：提取 PID（保留現有函數）**

現有的 `_extract_pid_from_output()` (第 352-357 行) 保持不變，因為 deploy.py 的輸出格式相同。

### Success Criteria

#### Automated Verification

- [x] Python 語法檢查通過：`python3 -m py_compile src/fwauto/deploy_operations.py` ✅
- [ ] 單元測試通過：`make test` 或 `uv run pytest tests/ -k deploy`
- [x] 類型檢查通過：`uv run pyright src/fwauto/deploy_operations.py` ✅
- [x] Linting 通過：`uv run python -m fwauto.scripts.lint` ✅

#### Manual Verification

- [x] 執行 `fwauto deploy` 成功部署到開發板 ✅
- [x] 確認 deploy_operations 不再直接執行 SSH/SCP（檢查 log 輸出）✅
- [x] 確認遠端日誌功能正常（檢查 config.toml 的 log_file_path）✅
- [x] 確認 background mode 的 PID 正確捕獲 ✅
- [x] 確認錯誤情況下的錯誤訊息正確顯示 ✅

**Implementation Note**: 完成此階段後，必須進行完整的實際部署測試，確認 deploy.py 調用正常且所有功能（包括遠端日誌）都能正常運作。確認無誤後再進入 Phase 3。

---

## Phase 3: 清理平台相關代碼

### Overview

移除 `deploy_operations.py` 中不再需要的平台相關函數，保持模組簡潔並真正做到平台無關。

**目標**：
- 移除所有直接執行 SSH/SCP 的函數
- 保留必要的公開介面（data classes、find_binary）
- 確保沒有其他地方依賴這些被移除的函數

### Dependency Check

在開始刪除前，必須確認沒有其他模組依賴這些函數。

**需要檢查的依賴**：
- `check_network_connectivity()` - 是否有其他地方調用？
- `deploy_binary()` - 是否有其他地方調用？
- `execute_on_board()` - 是否有其他地方調用？
- `_build_remote_script()` - 是否有其他地方調用？

**檢查方式**：
```bash
# 在專案根目錄執行
grep -r "check_network_connectivity" src/ tests/ --exclude-dir=__pycache__
grep -r "deploy_binary" src/ tests/ --exclude-dir=__pycache__
grep -r "execute_on_board" src/ tests/ --exclude-dir=__pycache__
grep -r "_build_remote_script" src/ tests/ --exclude-dir=__pycache__
```

### Changes Required

#### 1. deploy_operations.py - 移除平台相關函數

**檔案**：`src/fwauto/deploy_operations.py`

**移除以下函數**：

1. **check_network_connectivity()** (第 68-126 行)
2. **deploy_binary()** (第 177-212 行)
3. **execute_on_board()** (第 220-276 行)
4. **_build_remote_script()** (第 279-349 行)

**保留以下內容**：

1. **Data Classes** (第 28-54 行)：
   - `DeployConfig` - 部署配置
   - `DeployResult` - 部署結果

2. **Exception Classes** (第 62-65 行)：
   - `ConnectivityError` - 保留以維持向後兼容，但不再被 deploy_operations 拋出

3. **Binary Discovery** (第 134-169 行)：
   - `find_binary()` - deploy_node 需要此函數來取得 binary_name
   - `_is_executable()` - find_binary 的輔助函數

4. **High-Level Function** (第 365-423 行)：
   - `deploy_firmware()` - 已在 Phase 2 重構，保留
   - `_extract_pid_from_output()` - deploy_firmware 需要
   - `_extract_binary_name_from_output()` - Phase 2 新增，保留

#### 2. 更新 module docstring

**檔案**：`src/fwauto/deploy_operations.py`

**變更位置**：檔案開頭的 docstring（第 1-14 行）

**修改後的 docstring**：
```python
"""
Deploy operations module - Platform-independent deployment orchestration.

This module follows the Maketool Abstraction Layer pattern:
- Collects configuration from FWAuto (FirmwareState, ProjectConfig)
- Invokes platform-specific deploy scripts (deploy.py)
- Parses results and updates FirmwareState

Platform-specific logic (SSH, SCP, network checks) is handled by deploy scripts.

Design principles:
- Platform-independent (no direct SSH/SCP execution)
- Script invocation only (delegates to deploy.py)
- Configuration collection and result parsing
"""
```

#### 3. 更新 imports

**檔案**：`src/fwauto/deploy_operations.py`

**變更位置**：import 區塊（第 16-25 行）

**移除不需要的 imports**：
```python
import socket  # ❌ 移除 - check_network_connectivity 不再需要
import time    # ❌ 移除 - _build_remote_script 不再需要
```

**保留的 imports**：
```python
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Literal

from .logging_config import get_logger
```

#### 4. 清理後的檔案結構

**重構後的 deploy_operations.py 結構**：

```python
"""
Deploy operations module - Platform-independent deployment orchestration.
...
"""

import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Literal

from .logging_config import get_logger

logger = get_logger(__name__)


# ============================================
# Data Classes
# ============================================

@dataclass
class DeployConfig:
    """Configuration for deploy operations."""
    ...

@dataclass
class DeployResult:
    """Result of deploy operation."""
    ...


# ============================================
# Exceptions
# ============================================

class ConnectivityError(Exception):
    """Raised when network connectivity check fails (for backward compatibility)."""
    pass


# ============================================
# Binary Discovery
# ============================================

def find_binary(build_dir: Path) -> Path:
    """Find the first executable binary in build directory."""
    ...

def _is_executable(file: Path) -> bool:
    """Check if file is executable (cross-platform)."""
    ...


# ============================================
# Helper Functions
# ============================================

def _extract_binary_name_from_output(stderr: str) -> str:
    """Extract binary name from deploy.py stderr output."""
    ...

def _extract_pid_from_output(stderr: str) -> int | None:
    """Extract PID from deploy.py stderr output."""
    ...


# ============================================
# High-Level Deploy Operation
# ============================================

def deploy_firmware(build_dir: Path, config: DeployConfig) -> DeployResult:
    """
    Complete deploy operation by invoking deploy.py script.

    This function follows the Maketool Abstraction Layer pattern.
    """
    ...
```

**預期行數**：從 424 行減少到約 **200 行**（移除 ~220 行平台相關代碼）

### Success Criteria

#### Automated Verification

- [x] Python 語法檢查通過：`python3 -m py_compile src/fwauto/deploy_operations.py` ✅
- [ ] 單元測試通過：`make test` 或 `uv run pytest tests/`
- [x] 類型檢查通過：`uv run pyright src/fwauto/` ✅ (0 errors, 0 warnings)
- [x] Linting 通過：`uv run python -m fwauto.scripts.lint` ✅
- [x] 確認沒有 import 錯誤：`python3 -c "from src.fwauto.deploy_operations import deploy_firmware"` ✅
- [x] 依賴檢查：確認沒有其他檔案 import 被移除的函數 ✅ (grep 檢查完成)

#### Manual Verification

- [ ] 執行 `fwauto deploy` 成功部署到開發板（完整端到端測試）
- [ ] 確認部署行為與重構前完全相同
- [ ] 確認遠端日誌功能正常
- [ ] 確認錯誤情況下的錯誤訊息來自 deploy.py（更詳細的診斷）
- [ ] 檢查 deploy_operations.py 檔案大小確實減少約 50%

**Implementation Note**: 完成此階段後，執行完整的回歸測試，確保所有功能正常且無副作用。這是最後階段，完成後整個重構即完成。

---

## Testing Strategy

### Unit Tests

**測試重點**：
1. **deploy_operations.deploy_firmware()**：
   - 測試 deploy.py 命令構建正確
   - 測試 subprocess 調用成功
   - 測試 exit code 解析
   - 測試 stderr 解析（binary name、PID）
   - 測試錯誤處理（timeout、script 不存在）

2. **deploy_operations.find_binary()**：
   - 測試正確找到可執行檔
   - 測試 build 目錄不存在的錯誤處理
   - 測試沒有可執行檔的錯誤處理

**測試檔案位置**：`tests/test_deploy_operations.py`

**Mock 策略**：
- Mock `subprocess.run()` 避免實際執行 deploy.py
- Mock `Path.exists()` 測試各種檔案情況
- Mock `Path.iterdir()` 測試 binary 搜尋邏輯

### Integration Tests

**測試重點**：
1. **完整部署流程**：
   - 從 deploy_node → deploy_operations → deploy.py → 開發板
   - 確認遠端日誌正確寫入
   - 確認 background mode PID 捕獲

2. **錯誤路徑測試**：
   - 開發板離線時的錯誤處理
   - SSH 認證失敗的錯誤處理
   - Binary 不存在的錯誤處理

**測試環境需求**：
- AM62X 開發板（IP: 192.168.50.169）
- SSH 免密碼登入設定
- 編譯好的測試 binary

### Manual Testing Steps

#### 基本部署測試

1. **正常部署流程**：
   ```bash
   cd <AM62X project>
   fwauto build
   fwauto deploy
   ```
   - 確認部署成功
   - 確認程式在開發板上執行

2. **帶參數部署**：
   ```bash
   fwauto deploy --binary-args "test_mode"
   ```
   - 確認參數正確傳遞

3. **遠端日誌驗證**：
   ```bash
   fwauto deploy
   # 檢查 config.toml 的 log_file_path
   # SSH 到開發板確認日誌檔案存在
   ssh root@192.168.50.169 "cat /home/root/uart_<date>.log"
   ```

#### 錯誤情況測試

1. **開發板離線**：
   - 關閉開發板電源
   - 執行 `fwauto deploy`
   - 確認錯誤訊息清晰（來自 deploy.py）

2. **Binary 不存在**：
   - 刪除 build/ 目錄
   - 執行 `fwauto deploy`
   - 確認錯誤訊息清晰

3. **SSH 連線問題**：
   - 修改 config.toml 的 board_ip 為錯誤 IP
   - 執行 `fwauto deploy`
   - 確認錯誤訊息包含故障排除建議（來自 deploy.py）

## Performance Considerations

**預期效能影響**：
- 啟動 Python 解釋器的額外開銷：約 50-100ms
- 部署本身耗時（SCP 傳輸）：數秒到數十秒

**結論**：效能影響可忽略，架構清晰性優先。

## Migration Notes

### Backward Compatibility

**公開介面保持不變**：
- `deploy_firmware(build_dir, config)` 的簽名和返回值不變
- `DeployConfig` 和 `DeployResult` 的欄位不變
- deploy_node 不需要修改

**內部實作變更**：
- 移除的函數不在公開介面中（沒有 `__all__` 匯出）
- 保留 `ConnectivityError` 異常類別（雖然不再使用）以維持向後兼容

### Configuration Changes

**新增參數**：
- deploy.py 支援 `--remote-log-path` 參數
- 透過 CLI 參數或環境變數 `$REMOTE_LOG_PATH` 設定

**現有配置不受影響**：
- config.toml 的 `[deployment_config]` 欄位保持不變
- 使用者無需修改配置檔案

## References

### Original Research

- 研究文件：`.ai/prpi/research/2025-11-12-deploy-operations-abstraction-analysis.md`
- 關鍵發現：deploy_operations 違反 Maketool Abstraction Layer 原則
- 設計決策：移除平台相關診斷，保持 deploy_operations 平台無關

### Design Documents

- FWAuto Architecture：`CLAUDE.md` (Maketool Abstraction Layer 定義)
- Deploy Node Design：`docs/deploy-node-design.md`
- Deploy Guide：`docs/deploy-guide.md`

### Related Code

- Build Node (正確模式)：`src/fwauto/nodes/build.py:77-84`
- Execute Make Command：`src/fwauto/utils.py:79-165`
- Deploy Script (AM62X)：`src/fwauto/templates/sdk_am62x_linux/deploy/deploy.py`

### Historical Context

- Flash to Deploy Refactoring：`.ai/prpi/plans/2025-11-12-flash-to-deploy-refactoring.md`
- AM62X Deployment Config：`.ai/prpi/research/2025-11-06-am62x-deployment-config.md`

---

## 確認事項

完成計劃後，請確認以下事項：

- [x] 遵守 KISS 原則（最小化修改，避免過度設計）
- [x] 深思熟慮（詳細分析每個 Phase 的變更和風險）
- [x] 已與使用者討論並獲得確認（遠端日誌、STM32 介面、錯誤處理）
- [x] 僅進行計劃編寫，不進行任何實作
- [x] 每個 Phase 都有清晰的成功標準（Automated + Manual）
- [x] 包含 Mermaid 圖表說明架構變化
- [x] 參考了相關研究文件和設計決策

**計劃狀態**：✅ Draft 完成，待審查
