---
date: 2025-11-10T20:17:01+08:00
researcher: Claude (Sonnet 4.5)
git_commit: 12ffd5eb02a69e0b75266a4ab0a9fe8cf1f77ead
branch: log
repository: FWAauto
topic: "日誌產生跟路徑管理機制"
tags: [research, codebase, logging, uart-logs, firmware-logs, path-management, config-system, state-flow, append-mode, background-execution]
status: complete
last_updated: 2025-11-10
last_updated_by: Claude (Sonnet 4.5)
last_updated_note: "Integrated background execution research and design decisions"
---

# Research: 日誌產生跟路徑管理機制

**Date**: 2025-11-10T20:17:01+08:00
**Researcher**: Claude (Sonnet 4.5)
**Git Commit**: 12ffd5eb02a69e0b75266a4ab0a9fe8cf1f77ead
**Branch**: log
**Repository**: FWAauto

## Research Question

研究 FWAuto 中日誌的產生與路徑管理機制，特別聚焦於：
1. **兩種日誌的明確區分**：FWAuto 本身的 Python 日誌 vs 開發板上的 Firmware UART 日誌
2. **開發板日誌的產生流程**：目前如何產生、儲存、傳輸
3. **路徑在 State 中的管理**：如何在不同 nodes 之間傳遞
4. **Config 配置系統**：如何擴展以支援遠端日誌路徑配置
5. **背景執行需求**：Firmware 通常有 while(1) 迴圈，需要背景運行機制
6. **關鍵細節**：日誌檔案的 date 由本機決定，而非開發板日期

## Summary

FWAuto 目前有**兩種完全獨立的日誌系統**：

### 1. FWAuto Python 應用程式日誌 ✅ (已完整實作)
- **位置**：`.fwauto/logs/fwauto_YYYYMMDD_HHMMSS.log`（本機）
- **用途**：記錄 FWAuto 工作流程的執行過程（build, flash, AI 分析）
- **機制**：Python `logging` 模組 + 自訂 formatter
- **本次研究**：不涉及變更，僅作對照參考

### 2. 開發板 Firmware UART 日誌 ⚠️ (需要重新設計)
- **位置**：目前只存在於記憶體（SSH stdout/stderr），但未被使用
- **用途**：分析 Firmware 運行時行為（LED 閃爍、感測器數據等）
- **目前問題**：同步執行導致 while(1) 程式無法正常運行
- **本次研究重點**：需要背景執行 + 遠端日誌檔案持久化

**關鍵發現**：
- ✅ log_node 已支援遠端路徑（`user@host:path` 格式）
- ✅ Config 系統已有 deployment 配置機制
- ❌ flash.sh 使用同步執行，會阻塞 FWAuto 主執行緒
- ❌ while(1) firmware 會觸發 60 秒 timeout，可能被強制終止
- ❌ `flash_log` 被捕獲但從未被使用（architectural waste）
- ❌ `firmware_path` 在 State 中從未被設定（documentation-reality gap）
- ✅ 日誌 date 應由本機（flash_node）決定，符合現有 logging_config 模式

## Detailed Findings

### 架構概覽圖

```mermaid
graph TB
    subgraph "FWAuto Python 日誌 (本機)"
        A1[logging_config.py]
        A2[.fwauto/logs/fwauto_*.log]
        A3[所有 nodes 使用 get_logger]
        A1 --> A2
        A3 --> A1
    end

    subgraph "Firmware UART 日誌 (開發板) - 需重新設計"
        B1[build_node 編譯 binary]
        B2[flash_node 部署到開發板]
        B3["SSH 同步執行 ❌"]
        B4["阻塞等待 60 秒 ❌"]
        B5["flash_log 記憶體 (未使用) ❌"]
        B6[log_node 分析]
        B7["遠端日誌檔案 ❌ 未實作"]

        B1 --> B2
        B2 --> B3
        B3 --> B4
        B4 --> B5
        B4 -.-> B7
        B7 -.-> B6
    end

    style A2 fill:#90EE90
    style B3 fill:#FFB6C6
    style B4 fill:#FFB6C6
    style B5 fill:#FFB6C6
    style B7 fill:#FFB6C6
```

### Finding 1: FWAuto Python 日誌機制 (不涉及變更)

**檔案位置**：`src/fwauto/logging_config.py`

#### 日誌檔案命名規則

| 屬性 | 值 |
|------|---|
| **路徑模式** | `.fwauto/logs/fwauto_YYYYMMDD_HHMMSS.log` |
| **範例** | `.fwauto/logs/fwauto_20251110_201701.log` |
| **時間戳來源** | `datetime.now().strftime("%Y%m%d_%H%M%S")` |
| **設定位置** | `logging_config.py:90-95` |
| **目錄建立** | `log_dir.mkdir(parents=True, exist_ok=True)` |

#### 日誌系統架構

```mermaid
graph LR
    A[get_logger] --> B[fwauto hierarchy]
    B --> C[FileHandler]
    B --> D[ConsoleHandler 已停用]
    C --> E[".fwauto/logs/fwauto_*.log"]

    F[log_node_start] --> A
    G[log_node_success] --> A
    H[log_node_error] --> A
```

**重要特性**：
- ✅ 雙層 Handler 設計（File: DEBUG level, Console: 已停用）
- ✅ 自動 namespace 管理（`fwauto.nodes.build`, `fwauto.config`）
- ✅ 詳細 formatter：`[timestamp] [module] [file:line] level: message`
- ✅ 節點生命週期 helpers（`log_node_start`, `log_node_success`, `log_node_error`）

**與 Firmware 日誌的區別**：

| 維度 | FWAuto Python 日誌 | Firmware UART 日誌 |
|------|-------------------|-------------------|
| **位置** | `.fwauto/logs/fwauto_*.log` (本機) | 目標：開發板上；現況：記憶體但未使用 |
| **內容** | 工作流程執行記錄 | Firmware 運行時輸出 |
| **格式** | 結構化 (timestamp, module, level) | 原始 stdout/stderr |
| **用途** | 除錯 FWAuto 本身 | 分析 Firmware 行為 |
| **State 欄位** | 無（內部使用） | `log_file_path`, `log_analysis_result` |

**程式碼參考**：
- 日誌設定：`logging_config.py:58-108`
- 檔案命名：`logging_config.py:94`
- 節點 helpers：`logging_config.py:125-154`

---

### Finding 2: 開發板 Firmware 日誌的產生流程與同步執行問題

#### 當前流程圖（有嚴重問題）

```mermaid
sequenceDiagram
    participant User
    participant CLI
    participant FlashNode
    participant Makefile
    participant FlashScript
    participant SSH
    participant Board
    participant Firmware

    User->>CLI: fwauto flash
    CLI->>FlashNode: execute
    FlashNode->>Makefile: execute_make_command()
    Makefile->>FlashScript: flash.sh
    FlashScript->>SSH: scp binary
    SSH->>Board: 部署 binary
    FlashScript->>SSH: ssh execute (同步)
    SSH->>Board: ./binary $BINARY_ARGS
    Board->>Firmware: 開始執行

    alt Firmware 快速退出
        Firmware-->>Board: 執行完畢
        Board-->>SSH: stdout/stderr
        SSH-->>FlashScript: 傳回輸出
        FlashScript-->>Makefile: success
    else Firmware 有 while(1)
        Note over Firmware: 永遠不退出
        Note over SSH: 阻塞等待...
        Note over FlashNode: 阻塞 60 秒
        SSH->>SSH: 60 秒 timeout
        SSH-->>FlashNode: TimeoutExpired
        FlashNode->>FlashNode: process.kill()
        Note over Firmware: ⚠️ 可能繼續運行 (orphan)
    end

    Note over Board: ❌ 無遠端日誌檔案
    Note over FlashNode: flash_log 存在但從未被使用
```

#### 關鍵程式碼位置與問題分析

**flash.sh 同步執行邏輯** (`templates/sdk_am62x_linux/flash/flash.sh:67-87`)：

```bash
execute_on_board() {
    local binary_name="$1"
    local binary_args="${BINARY_ARGS:-}"

    # Line 80-84: 同步執行，會等待程式結束
    if ! ssh $SSH_OPTS "$BOARD_USER@$BOARD_IP" \
        "cd $DEPLOY_PATH && chmod +x ./$binary_name && ./$binary_name $binary_args"; then
        log_error "Execution failed"
        exit 1
    fi
}
```

**問題點**：
- ❌ **同步阻塞**：SSH 等待遠端命令完成才回傳
- ❌ **while(1) 無法運行**：有無限迴圈的 firmware 永遠不會退出
- ❌ **timeout 行為不明確**：60 秒後可能產生 orphan process
- ❌ **無輸出重導向**：stdout/stderr 不在開發板上保存

**flash_node 捕獲機制** (`nodes/flash.py:111-129`)：

```python
returncode, full_output = execute_make_command(
    makefile_path=makefile_path,
    target=target,
    project_root=project_root_path,
    timeout=60,  # ← 60 秒 timeout
    env_vars=deployment_env,
)

if returncode == 0:
    result_state = {
        **state,
        "flash_status": "success",
        "flash_errors": [],
        "flash_log": f"{full_output}\n{success_msg}",  # ← 包含 SSH stdout/stderr
    }
```

**execute_make_command 輸出捕獲** (`utils.py:117-160`)：

```python
popen_kwargs = {
    "stdout": subprocess.PIPE,      # 捕獲 stdout
    "stderr": subprocess.STDOUT,    # 合併 stderr 到 stdout
    "text": True,
    "encoding": "utf-8",
    "bufsize": 1,                   # Line buffered
    "env": env,
}

# Line 137: 阻塞等待輸出
stdout_data, _ = process.communicate(timeout=timeout)

# Line 141-142: 輸出到 console
for line in output_lines:
    print(line, end="", flush=True)

# Line 143-147: timeout 處理
except subprocess.TimeoutExpired:
    process.kill()  # ← 只殺死本機的 SSH 客戶端
    # 遠端 firmware 可能繼續運行（orphan process）
```

**Critical Finding: subprocess timeout 行為**

| 事件 | 本機行為 | 遠端行為 |
|------|---------|---------|
| 60 秒 timeout | `process.kill()` 殺死本機 SSH 客戶端 | ⚠️ Firmware **可能繼續運行** |
| SSH 斷開 | Python 收到 `TimeoutExpired` | Firmware 成為 orphan process |
| 下次 flash | 新的 SSH 連線 | ⚠️ 可能有**多個實例同時運行** |

#### flash_log 使用狀況分析

**搜尋結果**：
- `flash_log` 被設定在 `flash.py:128`
- **沒有任何 node 讀取** `flash_log`（搜尋 `state.get("flash_log")` 無結果）
- **沒有在 prompts 中使用**（只有 `build_log` 在 `fix_build_errors.md:18`）
- **graph routing 不使用**（只看 `flash_status`，不看 `flash_log`）

**結論**：`flash_log` 是 architectural waste（被捕獲但從未使用）

**目前的限制**：

| 問題 | 影響 | 位置 |
|------|------|------|
| 同步執行阻塞 | FWAuto 主執行緒被卡住 60 秒 | `flash.sh:80-84` |
| while(1) 無法運行 | Timeout 後可能被終止 | 整體架構 |
| Orphan process | 下次 flash 可能有多個實例衝突 | `utils.py:144` |
| flash_log 未使用 | 捕獲的輸出沒有價值 | 整個系統 |
| 無遠端檔案持久化 | 執行完畢後輸出無法再次存取 | `flash.sh` |
| 依賴 SSH 會話 | 長時間執行的 firmware 無法持續記錄 | 整體架構 |
| 無法事後分析 | log_node 無法讀取歷史日誌 | State flow |

#### log_node 遠端日誌支援 ✅

**遠端路徑偵測** (`nodes/log.py:190-212`)：

```python
def _is_remote_path(path: str) -> bool:
    """檢查路徑是否為遠端格式 (user@host:path)"""
    pattern = r"^[^@]+@[^:]+:.+"
    return bool(re.match(pattern, path))
```

**SCP 下載流程** (`nodes/log.py:243-323`)：

```mermaid
graph TD
    A[log_node 接收 log_file_path] --> B{_is_remote_path?}
    B -->|Yes| C[解析 user@host:path]
    C --> D["執行 SCP 下載"]
    D --> E["暫存到 .fwauto/logs/"]
    E --> F[AI 分析]
    F --> G[清理暫存檔]

    B -->|No| H[本機路徑]
    H --> F
```

**關鍵機制**：

| 功能 | 實作位置 | 說明 |
|------|---------|------|
| **路徑格式** | `_is_remote_path()` | 正則：`^[^@]+@[^:]+:.+` |
| **路徑解析** | `_parse_remote_path()` | 提取 user, host, path |
| **SCP 下載** | `_fetch_remote_log()` | 使用 SSH options from config |
| **暫存位置** | `.fwauto/logs/<hash>.log` | MD5 hash 避免衝突 |
| **清理機制** | `_cleanup_temp_files()` | finally block 清理 |

**程式碼參考**：
- SSH 執行：`flash.sh:80-84`
- 輸出捕獲：`utils.py:117-126`
- Timeout 處理：`utils.py:143-147`
- 遠端路徑支援：`log.py:190-212, 243-323`
- flash_node 狀態：`flash.py:124-129`

---

### Finding 3: State 中的路徑管理和資料流

#### 關鍵發現：`firmware_path` 從未被設定 ❌

**State 定義** (`state.py:67-70`)：

```python
firmware_path: Annotated[
    str | None,
    "Generated firmware hex file path (populated by build_node on success)",
]
```

**build_node 實際行為** (`nodes/build.py:87-99`)：

```python
if returncode == 0:
    result_state = {
        **state,
        "build_status": "success",
        "build_errors": [],
        "build_log": f"{full_output}\n✅ {success_msg}",
        # ❌ 從未設定 firmware_path
    }
```

**結論**：Documentation-Reality Gap
- 註解說「populated by build_node on success」
- 實際上 build_node 從未設定此欄位
- CLI 初始化為 `None`（`cli.py:95, 186, 277, 327, 369, 413`）

#### State 路徑欄位使用狀況

| 欄位 | 設定者 | 使用者 | 當前值 | 用途 |
|------|-------|--------|--------|------|
| `firmware_path` | ❌ 無 | ❌ 無 | `None` (永遠) | 理論上：build 產生的 binary 路徑 |
| `log_file_path` | CLI (使用者) | log_node | User-provided | UART 日誌路徑（本機或遠端） |
| `build_status` | build_node | graph routing | success/error | 控制流程路由 |
| `flash_status` | flash_node | graph routing | success/error | 控制流程路由 |
| `build_log` | build_node | AI prompts | Build output | AI 修復錯誤 |
| `flash_log` | flash_node | ❌ 無 | Flash output | **從未被使用** |

#### State 資料流圖

```mermaid
graph LR
    subgraph "CLI 初始化"
        A1[執行 fwauto flash]
        A2["初始化 State<br/>(firmware_path=None)"]
    end

    subgraph "Build Node"
        B1[編譯 binary]
        B2["設定 build_status<br/>build_log"]
        B3["❌ 未設定 firmware_path"]
    end

    subgraph "Flash Node"
        C1[讀取 config]
        C2["執行 flash.sh (同步)"]
        C3["設定 flash_status<br/>flash_log (未使用)"]
        C4["❌ 未設定 log_file_path"]
    end

    subgraph "Log Node"
        D1["讀取 log_file_path<br/>(使用者提供)"]
        D2[分析日誌]
        D3["設定 log_analysis_result"]
    end

    A1 --> A2
    A2 --> B1
    B1 --> B2
    B2 --> B3
    B3 --> C1
    C1 --> C2
    C2 --> C3
    C3 --> C4
    C4 --> D1
    D1 --> D2
    D2 --> D3
```

**Graph 路由邏輯** (`graph.py:92-127`)：

```python
def route_after_build(state: FirmwareState) -> str:
    """根據 build_status 決定下一步"""
    if state["build_status"] == "success":
        return "flash"  # 成功 → flash

    # 失敗 → 檢查重試次數
    if state["iteration"] < state["max_retries"]:
        return "ai_brain"  # AI 修復

    return END  # 超過重試次數 → 結束
```

**重要觀察**：
- 路由**只使用 status 欄位**（build_status, flash_status）
- **不使用 path 欄位**（firmware_path, log_file_path）
- Path 欄位是**資料傳遞**，非控制流程

**程式碼參考**：
- State 定義：`state.py:67-70`
- build_node 輸出：`build.py:87-99`
- CLI 初始化：`cli.py:95, 186, 277, 327, 369, 413`
- Graph 路由：`graph.py:92-127`

---

### Finding 4: Config 配置系統的擴展點

#### ProjectConfig 屬性定義模式

**模式範例** (`config.py:67-85`)：

```python
@property
def build_makefile(self) -> str:
    """Build Makefile 路徑，預設為 .fwauto/build/Makefile"""
    return self.data.get("build", {}).get("makefile", ".fwauto/build/Makefile")

@property
def flash_target(self) -> str:
    """Flash target 名稱，預設為 flash"""
    return self.data.get("flash", {}).get("target", "flash")
```

**模式特徵**：

| 元素 | 說明 | 範例 |
|------|------|------|
| **Decorator** | `@property` 唯讀屬性 | - |
| **Docstring** | 描述功能 + 預設值 | "Build Makefile 路徑，預設為..." |
| **Type Hint** | 明確型別（str, str \| None, bool） | `-> str` |
| **Chained .get()** | 巢狀字典存取 + 預設值 | `.get("build", {}).get("makefile", default)` |

#### Deployment Config 繼承機制

**兩層繼承架構** (`config.py:119-138`)：

```mermaid
graph TD
    A[ProjectConfig.deployment_config] --> B{有 Project scope?}
    B -->|Yes| C["讀取 .fwauto/config.toml<br/>[deployment.sdk_type]"]
    B -->|No| D["讀取 ~/.fwauto/config.toml<br/>(UserConfig)"]
    C --> E[返回 deployment dict]
    D --> E

    F[board_ip property] --> A
    G[board_user property] --> A
    H[deploy_path property] --> A
    I[ssh_options property] --> A
```

**繼承邏輯**：

```python
@property
def deployment_config(self) -> dict[str, Any]:
    """優先從 Project Config 讀取，否則從 User Config 繼承"""
    sdk_type = self.sdk_type
    if not sdk_type:
        return {}

    # 1. Project scope (優先)
    project_deployment = self.data.get("deployment", {}).get(sdk_type, {})
    if project_deployment:
        return project_deployment

    # 2. User scope (繼承)
    user_deployment = self.user_config.get_deployment_config(sdk_type)
    return user_deployment if user_deployment else {}
```

**Deployment 屬性範例** (`config.py:141-159`)：

| 屬性 | 型別 | 預設值 | 說明 |
|------|------|--------|------|
| `board_ip` | str | "192.168.50.170" | 開發板 IP |
| `board_user` | str | "root" | SSH 使用者 |
| `deploy_path` | str | "/home/root" | 遠端部署目錄 |
| `ssh_options` | str | "-o StrictHostKeyChecking=no ..." | SSH 選項 |

#### Config → Makefile 環境變數傳遞

**Flash Node 傳遞機制** (`nodes/flash.py:96-117`)：

```mermaid
sequenceDiagram
    participant FlashNode
    participant Config
    participant Utils
    participant Subprocess
    participant FlashScript

    FlashNode->>Config: 讀取 deployment 屬性
    Config-->>FlashNode: board_ip, board_user, etc.
    FlashNode->>FlashNode: 組裝 deployment_env dict
    FlashNode->>Utils: execute_make_command(env_vars=...)
    Utils->>Utils: 合併 os.environ + env_vars
    Utils->>Subprocess: Popen(..., env=merged_env)
    Subprocess->>FlashScript: 執行 flash.sh
    FlashScript->>FlashScript: 讀取環境變數 ${BOARD_IP}
```

**程式碼流程**：

```python
# 1. Flash Node 組裝環境變數 (flash.py:96-106)
deployment_env = {
    "BOARD_IP": config.board_ip,
    "BOARD_USER": config.board_user,
    "DEPLOY_PATH": config.deploy_path,
    "SSH_OPTIONS": config.ssh_options,
}
if binary_args:
    deployment_env["BINARY_ARGS"] = binary_args

# 2. Utils 合併環境變數 (utils.py:103-110)
env = os.environ.copy()
if env_vars:
    env.update(env_vars)
    logger.info(f"Additional env vars: {list(env_vars.keys())}")

# 3. Subprocess 傳遞環境 (utils.py:117-126)
popen_kwargs = {
    "env": env,
    # ... other kwargs
}
process = subprocess.Popen(cmd, **popen_kwargs)

# 4. Flash Script 讀取環境變數 (flash.sh:10-14)
BOARD_IP="${BOARD_IP:-192.168.50.170}"
BOARD_USER="${BOARD_USER:-root}"
DEPLOY_PATH="${DEPLOY_PATH:-/home/root}"
```

#### 擴展點：遠端日誌路徑配置

**推薦配置結構**（與現有模式一致）：

```toml
# .fwauto/config.toml (Project scope)
[sdk]
type = "am62x"

[deployment.am62x]
board_ip = "192.168.50.170"
board_user = "root"
deploy_path = "/home/root"
ssh_options = "-o StrictHostKeyChecking=no ..."

# 新增：遠端日誌路徑配置
remote_log_pattern = "~/.fwauto/logs/uart-{date}.log"
remote_log_enabled = true
```

**或 User scope** (`~/.fwauto/config.toml`)：

```toml
[deployment.am62x]
# ... 其他部署配置
remote_log_pattern = "~/.fwauto/logs/uart-{date}.log"
```

**新增屬性建議**：

```python
# config.py 新增
@property
def remote_log_pattern(self) -> str | None:
    """
    遠端日誌檔案路徑 pattern

    支援變數：
    - {date}: 本機日期 YYYYMMDD
    - {datetime}: 本機日期時間 YYYYMMDD_HHMMSS

    範例：
    - ~/.fwauto/logs/uart-{date}.log
    - /var/log/firmware/uart-{datetime}.log
    """
    return self.deployment_config.get("remote_log_pattern")

@property
def remote_log_enabled(self) -> bool:
    """是否啟用遠端日誌自動擷取"""
    return self.deployment_config.get("remote_log_enabled", False)
```

**設計優勢**：

| 特性 | 優勢 |
|------|------|
| **與 deployment 一致** | 使用相同的 SSH 憑證（board_ip, board_user, ssh_options） |
| **支援繼承** | Project 可覆寫 User 預設值 |
| **向下相容** | 若無配置，預設為 disabled |
| **彈性 pattern** | 支援變數替換（date, datetime） |

**整合到現有流程**：

```mermaid
graph LR
    A[Config.remote_log_pattern] --> B[Flash Node]
    B --> C["渲染路徑<br/>(本機日期)"]
    C --> D[傳遞 REMOTE_LOG_PATH]
    D --> E[flash.sh 背景執行 + 重導向]
    E --> F["設定 State.log_file_path<br/>(user@host:path)"]
    F --> G[Log Node]
    G --> H["_is_remote_path<br/>(已支援)"]
    H --> I["_fetch_remote_log<br/>(SCP 下載)"]
    I --> J[AI 分析]
```

**程式碼參考**：
- 屬性模式：`config.py:67-85`
- Deployment 繼承：`config.py:119-138`
- 環境變數傳遞：`flash.py:96-117`, `utils.py:103-126`
- Flash script 讀取：`flash.sh:10-14, 69, 81`

---

### Finding 5: 背景執行需求與技術方案

#### 問題分析：同步執行的限制

**典型 Firmware 結構**：

```c
int main() {
    led_init();
    sensor_init();

    while(1) {  // 無限迴圈
        led_blink();
        read_sensor();
        sleep(1);
    }

    return 0;  // 永遠不會到這裡
}
```

**當前流程的問題**：

```mermaid
sequenceDiagram
    participant FWAuto
    participant SSH
    participant Firmware

    FWAuto->>SSH: ssh ... "./binary"
    SSH->>Firmware: 啟動程式

    Note over Firmware: while(1) 執行中...
    Note over SSH: 阻塞等待程式結束
    Note over FWAuto: 主執行緒被卡住

    Note over FWAuto: 60 秒後...
    FWAuto->>FWAuto: process.kill()
    Note over FWAuto: 殺死本機 SSH 客戶端
    Note over Firmware: ⚠️ 可能繼續運行 (orphan)

    Note over FWAuto: 下次 flash...
    Note over Firmware: ⚠️ 多個實例同時運行
```

**問題總結**：

| 問題 | 影響 | 嚴重度 |
|------|------|--------|
| 同步執行阻塞 | FWAuto 主執行緒被卡住 60 秒 | 🔴 高 |
| while(1) 無法運行 | Timeout 後可能被終止 | 🔴 高 |
| Orphan process | 遠端可能殘留多個實例 | 🟡 中 |
| 無法持續記錄日誌 | SSH 斷開後無法捕獲輸出 | 🔴 高 |

#### 背景執行技術方案

**SSH 背景執行選項比較**：

| 方法 | 說明 | 適用情境 | 推薦度 |
|------|------|---------|--------|
| `nohup ... &` | 遠端命令背景執行，忽略 HUP 信號 | **Firmware 持續運行** | ⭐⭐⭐⭐⭐ |
| `ssh -f` | SSH 在執行命令前進入背景 | X11 程式、需要密碼認證 | ⭐⭐ |
| `screen/tmux` | 建立持久化 session | 需要後續 attach 的情境 | ⭐⭐⭐ |

**推薦方案：nohup 背景執行**

```bash
# 核心命令
ssh $SSH_OPTS "$BOARD_USER@$BOARD_IP" \
    "nohup ./$binary_name $binary_args >> $REMOTE_LOG_PATH 2>&1 &"
```

**優點**：
- ✅ SSH 立即回傳（exit code 0 表示啟動成功）
- ✅ Firmware 在背景持續運行
- ✅ 不受 SSH 斷開影響
- ✅ 日誌寫入遠端檔案
- ✅ 簡單直接（KISS 原則）

**缺點與解決方案**：

| 缺點 | 解決方案 | 優先級 |
|------|---------|--------|
| 無法捕獲立即錯誤（binary 不存在） | 使用 here-doc + set -euo pipefail | P0 |
| 重複執行導致多個實例 | 啟動前 `pkill -f $binary_name` | P0 |
| 無法確認程式真的跑起來 | sleep 1 + kill -0 驗證（可選） | P1 |

#### 完整背景執行方案

**推薦實作（整合所有需求）**：

```bash
execute_on_board() {
    local binary_name="$1"
    local binary_args="${BINARY_ARGS:-}"
    local remote_log="${REMOTE_LOG_PATH}"

    log_info "Starting $binary_name in background..."

    # 使用 here-doc 傳遞多行命令
    if ! ssh $SSH_OPTS "$BOARD_USER@$BOARD_IP" bash <<'EOF'
        set -euo pipefail  # 嚴格錯誤處理

        # 停止舊實例（避免重複）
        pkill -f "$1" 2>/dev/null || true

        # 建立日誌目錄
        mkdir -p "$(dirname "$2")"

        # 寫入分隔符（追加模式）
        {
            echo ""
            echo "========================================"
            echo "Execution at $(date '+%Y-%m-%d %H:%M:%S')"
            echo "Binary: $1"
            echo "Args: $3"
            echo "========================================"
        } >> "$2"

        # 背景執行 + 日誌重導向
        cd "$DEPLOY_PATH"
        chmod +x "./$1"
        nohup "./$1" $3 >> "$2" 2>&1 &
        local pid=$!

        # 等待 1 秒驗證程式是否啟動成功
        sleep 1
        if kill -0 $pid 2>/dev/null; then
            echo "Binary started successfully (PID: $pid)" >&2
            exit 0
        else
            echo "Binary crashed immediately" >&2
            exit 1
        fi
EOF
    "$binary_name" "$remote_log" "$binary_args"
    then
        log_error "Failed to start $binary_name"
        exit 1
    fi

    log_success "Binary started in background"
    log_info "Logs: $BOARD_USER@$BOARD_IP:$remote_log"
}
```

**關鍵設計點**：

1. **pkill -f**: 避免重複實例（殺死舊的同名程式）
2. **set -euo pipefail**: 嚴格錯誤處理（任何命令失敗就退出）
3. **here-doc**: 安全傳遞參數（避免 shell 注入）
4. **mkdir -p**: 確保日誌目錄存在
5. **追加模式 (>>)**: 同一天的日誌追加到同一檔案
6. **分隔符**: 區分不同次執行
7. **sleep 1 + kill -0**: 快速驗證程式啟動成功（1 秒內完成）
8. **nohup ... &**: 背景執行，不受 SSH 斷開影響

#### 新的執行流程

```mermaid
sequenceDiagram
    participant FWAuto
    participant SSH
    participant Board
    participant Firmware
    participant LogFile

    Note over FWAuto: 使用者執行 fwauto flash

    FWAuto->>SSH: SCP 上傳 binary (~2秒)
    SSH->>Board: 部署到 /home/root

    FWAuto->>SSH: 執行背景啟動命令
    SSH->>Board: pkill -f binary (清理舊實例)
    SSH->>Board: mkdir -p ~/.fwauto/logs
    SSH->>Board: 寫入分隔符到 log
    SSH->>Board: nohup ./binary >> log 2>&1 &

    Board->>Firmware: 啟動 firmware (背景)
    Board->>Board: sleep 1
    Board->>Board: kill -0 $pid (驗證)

    alt 程式啟動成功
        Board-->>SSH: exit 0 + PID
        SSH-->>FWAuto: 回報成功 (~3-4秒)
    else 程式立即 crash
        Board-->>SSH: exit 1
        SSH-->>FWAuto: 回報失敗
    end

    Note over FWAuto: SSH 斷開，FWAuto 繼續工作

    Note over Firmware: 繼續在背景運行 while(1)
    loop 持續運行
        Firmware->>LogFile: 寫入 UART 輸出
        LogFile->>LogFile: 追加到 uart-20251110.log
    end

    Note over FWAuto: 稍後執行 fwauto log
    FWAuto->>SSH: SCP 下載日誌檔案
    SSH->>LogFile: 讀取 uart-20251110.log
    LogFile-->>SSH: 完整日誌內容
    SSH-->>FWAuto: 傳回本機
    FWAuto->>FWAuto: AI 分析日誌
```

**時間分析**：

| 階段 | 時間 | 說明 |
|------|------|------|
| SCP 上傳 binary | ~2 秒 | 取決於檔案大小和網路速度 |
| SSH 建立連線 | ~0.5 秒 | 取決於網路延遲 |
| pkill + mkdir | ~0.5 秒 | 遠端命令執行 |
| 寫入分隔符 | ~0.2 秒 | 檔案 I/O |
| nohup 啟動 | ~0.3 秒 | 程式啟動 |
| sleep 1 驗證 | 1 秒 | 確認程式沒有立即 crash |
| SSH 回傳 | ~0.5 秒 | 網路傳輸 |
| **總計** | **~5 秒** | 可接受的延遲 |

#### timeout 參數調整

**當前值 vs 建議值**：

| 元件 | 當前 timeout | 建議 timeout | 理由 |
|------|-------------|-------------|------|
| `flash.py:115` | 60 秒 | **10 秒** | 背景執行只需 5 秒，10 秒足夠容錯 |

**程式碼參考**：
- 當前同步執行：`flash.sh:80-84`
- subprocess timeout：`utils.py:137, 143-147`
- flash_node timeout：`flash.py:115`

---

## Architecture Insights

### 1. 兩種日誌系統的清晰邊界

FWAuto 的設計將**開發工具的日誌**與**目標設備的日誌**完全分離：

| 維度 | FWAuto Python 日誌 | Firmware UART 日誌 |
|------|-------------------|-------------------|
| **職責** | 記錄工作流程執行 | 記錄 Firmware 行為 |
| **儲存位置** | 本機 `.fwauto/logs/` | 開發板 `~/.fwauto/logs/` |
| **檔名格式** | `fwauto_YYYYMMDD_HHMMSS.log` | `uart-YYYYMMDD.log` |
| **管理機制** | `logging_config.py` | flash.sh + config |
| **State 欄位** | 無（內部） | `log_file_path`, `log_analysis_result` |
| **分析工具** | 人工閱讀 | log_node + Claude AI |
| **執行模式** | 同步（記錄 FWAuto 自己的行為） | 背景（Firmware 持續運行） |

### 2. 模組化設計的擴展性

**Maketool Abstraction Pattern**：
- Python 層：工作流程編排、狀態管理、AI 整合
- Makefile/Script 層：工具鏈細節（Keil, GCC, SSH, SCP）
- **優勢**：新增背景執行只需修改 flash.sh，Python 層變動最小

**Config 繼承機制**：
- User Config：全域預設值（所有專案共用）
- Project Config：專案特定覆寫
- **優勢**：新增 `remote_log_pattern` 可自然融入現有架構

**State 資料流**：
- Nodes 透過回傳 dict 更新 State
- 路由邏輯只看 status 欄位，不看 path 欄位
- **優勢**：新增 log_file_path 傳遞不影響控制流程

### 3. 當前架構的 Gaps

#### Gap 1: firmware_path Documentation-Reality Gap

**問題**：
- State 註解說「populated by build_node on success」
- 實際上從未被設定

**影響**：
- 原計畫需要 binary 名稱來產生日誌檔名
- 已透過設計決策解決（不使用 app_name）

#### Gap 2: 同步執行導致 while(1) 程式無法運行 🔴

**問題**：
- flash.sh 同步執行，等待程式退出
- 有 while(1) 的 firmware 永遠不會退出
- 60 秒 timeout 後可能產生 orphan process

**影響**：
- 典型 firmware 無法正常運行
- 開發板上可能有多個實例同時運行
- FWAuto 主執行緒被阻塞

**解決方案**：背景執行 + 遠端日誌檔案

#### Gap 3: flash_log 被捕獲但從未使用

**問題**：
- SSH 輸出被存入 `flash_log`
- 沒有任何 node、prompt、routing 使用它

**影響**：
- Architectural waste（浪費資源捕獲無用資料）
- 誤導性設計（讓人以為有用）

**解決方案**：改為背景執行，flash_log 語意變更為「啟動狀態」

#### Gap 4: Flash → Log 自動連接缺失

**問題**：
- flash_node 不設定 `log_file_path`
- 使用者需手動指定 `--log-path`

**影響**：
- 使用體驗不連貫
- 無法自動分析剛燒錄的 firmware 輸出

**解決方案**：flash_node 設定 `log_file_path` 到 State

### 4. 設計原則的一致性

**KISS 原則體現**：
- ✅ 配置扁平化（少量 properties，預設值明確）
- ✅ 單一職責（config 讀取、flash 執行、log 分析分離）
- ✅ 最小變動（利用現有機制，不引入新架構）
- ✅ 背景執行使用標準 nohup（不引入 screen/tmux 複雜度）

**Anchor Mode 的成功**：
- 從任何子目錄執行指令，自動找到 `.fwauto/`
- 類似設計可應用於日誌：自動找到「最新的日誌」

**時間戳策略統一**：
- FWAuto 日誌：本機時間戳（`logging_config.py:94`）
- Firmware 日誌：本機時間戳（一致性）
- **理由**：開發板時鐘可能未同步，本機時間更可靠

---

## Design Decisions

### 決策 1：不使用 app_name ✅

**原始考量**：
- 原本計畫日誌檔名包含 binary 名稱：`{app_name}-{date}.log`
- 需要從 build_log 解析或修復 firmware_path

**最終決定**：
- **簡化路徑 pattern**：`uart-{date}.log`（不含 app_name）
- **優勢**：
  - 實作更簡單（不需要解析 binary 名稱）
  - 避免 firmware_path 的 documentation-reality gap 問題
  - 符合 KISS 原則

**Config 預設值**：
```toml
[deployment.am62x]
remote_log_pattern = "~/.fwauto/logs/uart-{date}.log"
```

### 決策 2：追加模式（Append Mode）⭐

**選項比較**：
- **選項 A（覆蓋模式）**：同一天覆蓋，只保留最後一次
- **選項 B（時間戳模式）**：每次執行產生新檔案 `uart-{datetime}.log`
- **選項 C（追加模式）**：同一天追加到同一檔案 ← **採用**

**最終決定**：採用追加模式（Append Mode）

**實作細節**：
```bash
# 每次執行前寫入分隔符
{
    echo ""
    echo "========================================"
    echo "Execution at $(date '+%Y-%m-%d %H:%M:%S')"
    echo "Binary: $binary_name"
    echo "Args: $binary_args"
    echo "========================================"
} >> $REMOTE_LOG_PATH

# 追加輸出（使用 >>）
nohup ./$binary_name $binary_args >> $REMOTE_LOG_PATH 2>&1 &
```

**優勢**：

| 優勢 | 說明 |
|------|------|
| **保留完整歷史** | 同一天的所有執行都在同一檔案 |
| **檔案數量可控** | 每天只有一個檔案，不會累積太多檔案 |
| **時間序列分析** | 可以看出行為變化趨勢 |
| **除錯友善** | 不會遺失任何輸出 |
| **符合開發流程** | 一天內多次調試的常見情境 |

**範例輸出**：
```log
========================================
Execution at 2025-11-10 10:00:15
Binary: led_app
Args: on
========================================
LED initialized
LED turned on
LED brightness: 100%

========================================
Execution at 2025-11-10 14:30:42
Binary: led_app
Args: off
========================================
LED initialized
LED turned off

========================================
Execution at 2025-11-10 18:15:03
Binary: led_app
Args: blink
========================================
LED initialized
LED blinking started (running in background...)
```

### 決策 3：背景執行 + 快速驗證 ⚡

**需求**：
- SSH 立即回傳（不阻塞 FWAuto 主執行緒）
- 確認程式有跑起來（不只是 nohup 成功）
- 支援 while(1) 無限迴圈的 firmware

**最終決定**：
- 使用 `nohup ... &` 背景執行
- 等待 1 秒後用 `kill -0 $pid` 驗證
- SSH 在 ~3-4 秒內回傳

**驗證邏輯**：
```bash
nohup ./$binary_name >> $log 2>&1 &
local pid=$!

sleep 1  # 等待 1 秒

if kill -0 $pid 2>/dev/null; then
    echo "Binary started successfully (PID: $pid)"
    exit 0  # 啟動成功
else
    echo "Binary crashed immediately"
    exit 1  # 啟動失敗
fi
```

**可偵測的失敗**：
- ✅ Binary 檔案不存在
- ✅ 沒有執行權限
- ✅ 啟動後立即 segfault
- ✅ 動態連結庫缺失
- ❌ 執行 30 秒後才 crash（不在驗證範圍）

**時間權衡**：
- 0 秒驗證：無法偵測失敗 ❌
- 1 秒驗證：偵測 90% 的立即失敗 ✅
- 5 秒驗證：更可靠，但太慢 ⚠️

### 決策 4：pkill 清理舊實例 🧹

**問題**：
- 連續執行 `fwauto flash` 多次
- 每次都啟動新的 firmware 實例
- 多個實例爭搶硬體資源（GPIO, UART）

**最終決定**：
- 啟動前執行 `pkill -f $binary_name`
- 使用 `|| true` 避免找不到程式時報錯

**實作**：
```bash
# 停止舊實例（避免重複）
pkill -f "$binary_name" 2>/dev/null || true
```

**安全性考量**：
- `pkill -f` 匹配完整命令列（包含參數）
- 只殺死同名 binary，不影響其他程式
- `2>/dev/null` 抑制「找不到程式」的錯誤訊息
- `|| true` 確保即使 pkill 失敗也繼續執行

### 決策 5：日誌路徑由本機決定 📅

**問題**：
- 開發板時鐘可能未同步
- 日誌檔名應該反映「flash 時間」而非「執行時間」

**最終決定**：
- 在 flash_node (本機) 渲染路徑 pattern
- 使用本機日期生成檔名
- 透過環境變數傳遞給 flash.sh

**實作**：
```python
# flash_node (本機)
from datetime import datetime

date_str = datetime.now().strftime("%Y%m%d")
remote_log_path = config.remote_log_pattern.format(date=date_str)

deployment_env["REMOTE_LOG_PATH"] = remote_log_path
```

**一致性**：
- FWAuto 日誌：`fwauto_20251110_201701.log`（本機時間）
- Firmware 日誌：`uart-20251110.log`（本機日期）
- 兩者都使用本機時間戳

### 決策 6：log_file_path 持久化到 config.toml 💾

**問題**：
- `fwauto run` 需要 State 傳遞 log_file_path（同一次執行）
- `fwauto flash` 後稍後執行 `fwauto log` 需要記住上次路徑（跨執行）
- 如何在 State（記憶體）和持久化之間取得平衡？

**方案比較**：

| 方案 | 優點 | 缺點 | 評估 |
|------|------|------|------|
| **A. 只用 State** | 簡單、快速 | 無法跨執行 | ❌ 不滿足需求 |
| **B. 新增 state_cache.json** | 語意清晰、分離關注點 | 多一個檔案、增加複雜度 | ⚠️ 違反 KISS |
| **C. 寫入 config.toml** | 最簡單、統一管理 | 配置與狀態混合 | ✅ **採用** |

**最終決定**：寫入 config.toml（方案 C）

**設計理念**：
- **KISS 至上**：不新增檔案、不新增模組
- **語意明確**：欄位名稱 `last_log_file_path` 清楚標示為「程式自動更新」
- **使用者友善**：可見、可手動編輯、可理解

**Config 結構**：
```toml
[deployment.am62x]
board_ip = "192.168.50.170"
board_user = "root"
deploy_path = "/home/root"
ssh_options = "-o StrictHostKeyChecking=no ..."
remote_log_enabled = true
remote_log_pattern = "~/uart-{date}.log"

# 程式自動更新（使用者也可以手動修改）
last_log_file_path = "root@192.168.50.170:/home/root/uart-2025-11-10.log"
```

**優先順序設計**（log_node 取得路徑）：
1. **CLI 參數** (`--log-path`) - 最高優先級（使用者明確指定）
2. **State 記憶體** (來自 flash_node) - 中優先級（同一次執行）
3. **Config 持久化** (`last_log_file_path`) - 最低優先級（跨執行 fallback）

**實作要點**：
```python
# config.py 新增
@property
def last_log_file_path(self) -> str | None:
    """上次 flash 產生的日誌路徑（程式自動寫入）"""
    return self.deployment_config.get("last_log_file_path")

def update_last_log_path(self, log_path: str) -> None:
    """更新 last_log_file_path 到 config.toml"""
    # 使用 toml 模組寫入

# flash_node 呼叫
config.update_last_log_path(remote_log_path)

# log_node fallback
log_file_path = (
    state.get("log_file_path")           # State 優先
    or config.last_log_file_path         # Config fallback
    or None                              # 錯誤
)
```

**使用情境**：

| 情境 | 路徑來源 | 說明 |
|------|---------|------|
| `fwauto run "問題"` | State 傳遞 | flash_node → log_node（同一次執行） |
| `fwauto flash` 後 `fwauto log "問題"` | Config fallback | 跨執行，從 config 讀取 |
| `fwauto log "問題" --log-path xxx` | CLI 參數 | 手動指定，分析舊日誌或其他板子 |

**優勢**：
- ✅ 滿足所有使用情境（run/flash/log）
- ✅ 最 KISS（不新增檔案、不新增複雜機制）
- ✅ 透明可見（使用者可以看到和編輯）
- ✅ 向下相容（欄位可選，不影響現有專案）

**權衡**：
- ⚠️ Config 語意混合（配置 + 狀態）
- ⚠️ Git 可能產生 diff（建議加入 .gitignore）
- ✅ 但符合「實用主義」和「最簡設計」原則

---

## Technical Feasibility

### 方向 1: 背景執行機制

**可行性**：✅ 高度可行

**現有基礎**：
- ✅ flash.sh 已能執行 SSH 命令
- ✅ Bash 支援 nohup 和背景執行（`&`）
- ✅ 環境變數傳遞機制完整

**潛在修改點**：
- `flash.sh:67-87`：改寫 `execute_on_board()` 函數
- `flash.py:115`：調整 timeout 從 60 秒到 10 秒

**風險評估**：

| 風險 | 影響 | 緩解措施 | 評級 |
|------|------|---------|------|
| 立即 crash 偵測不到 | 誤報成功 | sleep 1 + kill -0 驗證 | 🟢 低 |
| 重複實例衝突 | 硬體資源競爭 | pkill -f 清理舊實例 | 🟢 低 |
| 遠端目錄不存在 | 日誌寫入失敗 | mkdir -p 建立目錄 | 🟢 低 |
| Here-doc 語法錯誤 | SSH 執行失敗 | 單元測試驗證 | 🟡 中 |

### 方向 2: 遠端日誌檔案產生

**可行性**：✅ 高度可行

**現有基礎**：
- ✅ Bash 支援輸出重導向（`>` 和 `>>`）
- ✅ 環境變數傳遞機制完整
- ✅ log_node 已支援 SCP 下載

**潛在修改點**：
- `flash.sh:80-84`：新增輸出重導向到 `$REMOTE_LOG_PATH`
- `config.py`：新增 `remote_log_pattern` 屬性
- `flash.py`：傳遞 `REMOTE_LOG_PATH` 環境變數

**風險評估**：

| 風險 | 影響 | 緩解措施 | 評級 |
|------|------|---------|------|
| 路徑權限問題 | 無法寫入 | 使用 `~/` 確保權限 | 🟢 低 |
| 日誌累積過多 | 磁碟空間耗盡 | 文檔建議定期清理 | 🟡 中 |
| Pattern 渲染錯誤 | 路徑不正確 | 單元測試驗證 | 🟢 低 |

### 方向 3: State 自動傳遞日誌路徑

**可行性**：✅ 可行

**現有基礎**：
- ✅ log_node 已支援遠端路徑（`user@host:path`）
- ✅ flash_node 已能更新 State
- ✅ Graph 會將 State 從 flash_node 傳給 log_node

**潛在修改點**：
- `flash.py:124-129`：新增 `"log_file_path": remote_log_path`
- 需從 config 取得 `remote_log_pattern` 並渲染

**風險評估**：

| 風險 | 影響 | 緩解措施 | 評級 |
|------|------|---------|------|
| Pattern 未配置 | log_file_path 為 None | 提供合理預設值 | 🟢 低 |
| 路徑格式錯誤 | log_node 無法解析 | 單元測試驗證格式 | 🟢 低 |

### 方向 4: 日誌路徑 Pattern 渲染

**可行性**：✅ 高度可行

**現有基礎**：
- ✅ Python 有 `str.format()` 和 `datetime.now()`
- ✅ flash_node 在本機執行（可取得本機時間）

**建議實作**：

```python
def render_log_path(
    pattern: str,
    board_user: str,
    board_ip: str
) -> str:
    """
    渲染遠端日誌路徑（簡化版：無需 app_name）

    Args:
        pattern: ~/.fwauto/logs/uart-{date}.log
        board_user: root
        board_ip: 192.168.50.170

    Returns:
        root@192.168.50.170:~/.fwauto/logs/uart-20251110.log
    """
    from datetime import datetime

    # 本機日期（非開發板日期）
    date_str = datetime.now().strftime("%Y%m%d")
    datetime_str = datetime.now().strftime("%Y%m%d_%H%M%S")

    # 替換變數
    expanded = pattern.format(
        date=date_str,
        datetime=datetime_str
    )

    # 組合遠端路徑
    return f"{board_user}@{board_ip}:{expanded}"
```

**風險評估**：低

---

## Open Questions

### Q1: 日誌 Rotation 策略

**問題**：遠端日誌會持續累積

**待探索**：
- 是否需要自動清理 N 天前的日誌？
- 清理邏輯應在哪裡執行？（flash.sh, 獨立腳本, cron job）
- 使用者是否需要手動管理？

**建議方向**：
- 短期：文檔說明手動清理方式（`find ~/.fwauto/logs -mtime +7 -delete`）
- 長期：提供 `fwauto clean` 命令

### Q2: firmware_path 的修復優先級

**問題**：是否應該修復 build_node 設定 firmware_path？

**考量**：
- **優先級**：短期已透過設計決策繞過（不使用 app_name）
- **跨平台一致性**：不同 SDK 的 binary 輸出路徑不同
- **影響範圍**：需修改 build_node + 各 SDK 的 Makefile

**建議**：P3 優先級（非必要，長期可考慮）

### Q3: 多開發板管理

**問題**：使用者可能有多塊開發板

**待探索**：
- 如何區分不同開發板的日誌？
- 是否需要在檔名中包含 board_ip？
- 例如：`uart-192.168.50.170-20251110.log`

**建議方向**：
- 短期：透過 project config 區分（不同專案連不同板子）
- 長期：支援 `{board_ip}` 變數

---

## Code References

### 關鍵檔案總覽

| 檔案 | 職責 | 行號範圍 |
|------|------|---------|
| `state.py` | State schema 定義 | 6-77 |
| `logging_config.py` | FWAuto Python 日誌系統 | 1-234 |
| `config.py` | 配置管理與繼承 | 43-213 |
| `user_config.py` | 使用者全域配置 | 10-66 |
| `nodes/build.py` | 編譯節點 | 21-133 |
| `nodes/flash.py` | 燒錄節點 | 21-163 |
| `nodes/log.py` | 日誌分析節點 | 22-408 |
| `utils.py` | Makefile 執行工具 | 79-165 |
| `graph.py` | 工作流程路由 | 16-193 |
| `templates/sdk_am62x_linux/flash/flash.sh` | AM62x 燒錄腳本 | 1-108 |

### 詳細程式碼位置表

| 功能 | 檔案:行號 | 說明 |
|------|----------|------|
| **FWAuto 日誌配置** | `logging_config.py:58-108` | `setup_logging()` 函數 |
| **日誌檔案命名** | `logging_config.py:94` | `fwauto_{timestamp}.log` |
| **節點 logging helpers** | `logging_config.py:125-154` | `log_node_start()` 等 |
| **State schema** | `state.py:6-77` | `FirmwareState` TypedDict |
| **firmware_path 定義** | `state.py:67-70` | 註解說明但從未設定 |
| **log_file_path 定義** | `state.py:63-64` | UART 日誌路徑 |
| **build_node 輸出** | `build.py:87-99` | 未設定 firmware_path |
| **flash_node 環境變數** | `flash.py:96-106` | deployment_env 組裝 |
| **flash_node timeout** | `flash.py:115` | 目前 60 秒，建議改 10 秒 |
| **execute_make_command** | `utils.py:79-165` | Makefile 執行與輸出捕獲 |
| **環境變數合併** | `utils.py:103-110` | `env.update(env_vars)` |
| **Popen 輸出捕獲** | `utils.py:117-126` | stdout/stderr 配置 |
| **subprocess timeout** | `utils.py:137, 143-147` | communicate() + kill() |
| **flash.sh 執行** | `flash.sh:67-87` | `execute_on_board()` |
| **SSH 同步執行** | `flash.sh:80-84` | 需改為背景執行 |
| **flash.sh 環境變數** | `flash.sh:10-14` | 讀取 BOARD_IP 等 |
| **log_node 路徑處理** | `log.py:42-86` | 本機/遠端路徑解析 |
| **遠端路徑偵測** | `log.py:190-212` | `_is_remote_path()` |
| **SCP 下載** | `log.py:243-323` | `_fetch_remote_log()` |
| **暫存目錄** | `log.py:325-337` | `.fwauto/logs/` 管理 |
| **ProjectConfig class** | `config.py:43-179` | 配置載入與屬性 |
| **屬性定義模式** | `config.py:67-85` | build/flash properties |
| **deployment_config** | `config.py:119-138` | 兩層繼承邏輯 |
| **Deployment properties** | `config.py:141-159` | board_ip, board_user 等 |
| **UserConfig** | `user_config.py:10-66` | 使用者全域配置 |
| **Graph 路由** | `graph.py:92-127` | `route_after_build()` 等 |
| **CLI 初始化** | `cli.py:95, 186, 277...` | firmware_path=None |

---

## Implementation Roadmap

### Phase 1: 背景執行機制（P0）

**目標**：支援 while(1) firmware 正常運行

**修改項目**：
1. `flash.sh:67-87` - 改寫 `execute_on_board()` 為背景執行
2. `flash.py:115` - 調整 timeout: 60 → 10
3. 單元測試 - 驗證 here-doc 語法

**驗收標準**：
- ✅ SSH 在 3-5 秒內回傳
- ✅ Firmware 在背景持續運行
- ✅ 可偵測立即 crash（1 秒驗證）
- ✅ pkill 清理舊實例

### Phase 2: 遠端日誌檔案（P0）

**目標**：日誌寫入開發板檔案

**修改項目**：
1. `config.py` - 新增 `remote_log_pattern` 屬性
2. `flash.py` - 渲染路徑並傳遞環境變數 `REMOTE_LOG_PATH`
3. `flash.sh` - 新增日誌重導向（追加模式 + 分隔符）
4. 單元測試 - 驗證路徑渲染

**驗收標準**：
- ✅ 日誌寫入 `~/.fwauto/logs/uart-{date}.log`
- ✅ 追加模式 + 分隔符
- ✅ 支援變數替換（{date}, {datetime}）

### Phase 3: State 自動傳遞（P1）

**目標**：Flash 後自動設定 log_file_path

**修改項目**：
1. `flash.py:124-129` - 設定 `log_file_path` 到 State
2. 整合測試 - 驗證 flash → log 工作流程

**驗收標準**：
- ✅ flash_node 設定 `log_file_path` 為遠端路徑
- ✅ log_node 可自動讀取並分析

### Phase 4: 文檔與清理（P2）

**目標**：使用者文檔與日誌管理

**修改項目**：
1. 文檔 - 背景執行說明
2. 文檔 - 日誌清理建議
3. Config 範例 - `remote_log_pattern` 配置

**驗收標準**：
- ✅ README 更新
- ✅ Config 範例文件

---

## Notes

### 研究方法

本研究透過 **5 個主要方向** 進行：
1. FWAuto Python 日誌機制（理解現有系統）
2. 開發板日誌流程（發現同步執行問題）
3. State 路徑管理（資料流分析）
4. Config 擴展點（架構可行性）
5. 背景執行需求（深度技術研究）

### 關鍵洞察

1. **兩種日誌的清晰區隔**是設計優勢，不應混淆
2. **同步執行是核心問題**，導致 while(1) 程式無法運行
3. **flash_log 是 architectural waste**，被捕獲但從未使用
4. **背景執行 + 遠端日誌**是唯一可行方案
5. **Maketool Abstraction** 使得修改集中在 shell 層
6. **log_node 已完整支援遠端路徑**，只缺產生機制

### 實作建議的優先順序

1. **P0**：修改 flash.sh 改為背景執行（核心問題）
2. **P0**：新增遠端日誌重導向（必要功能）
3. **P1**：新增 Config 屬性（remote_log_pattern）
4. **P1**：flash_node 設定 log_file_path 到 State
5. **P2**：文檔與使用者指南

### 設計決策總結

所有關鍵設計決策已確認：
- ✅ 不使用 app_name（簡化實作）
- ✅ 追加模式（保留完整歷史）
- ✅ 背景執行 + 1 秒驗證（平衡速度與可靠性）
- ✅ pkill 清理舊實例（避免衝突）
- ✅ 日誌路徑由本機決定（時間戳一致性）
- ✅ log_file_path 持久化到 config.toml（KISS 原則，不新增檔案）

---

**研究完成**：所有關鍵設計決策已確認，技術可行性已驗證，可進入 Plan 階段進行詳細實施規劃。
