# GitHub 最終版：主流程、排程與狀態機重構參考

查核日期：2026-10-14。來源：`origin/main`，commit `6b0b32cd013409a328a92c89b0d348b7e864337c`。

本次已執行 `git fetch origin --prune`，並以 `git ls-remote --symref origin HEAD` 確認遠端預設分支為 `main`。本機 HEAD 與 origin/main 的 ahead／behind 都是 0，因此沒有覆寫或重設工作區。這份文件描述該 commit 的程式，不能據此判斷車上目前燒錄哪個版本。

搭配閱讀：[轉彎與旗標詳解](FINAL_TURN_AND_FLAGS_REFERENCE.md)。本篇先建立全貌，再說明如何拆成你自己的狀態機。程式目前的行為與建議重構方案分章呈現。

## 1. 先固定版本事實

倉庫的 AGENTS.md、README 後段與部分程式註解保留了更早版本的敘述。以下以實際建置旗標與執行路徑為準；本次只新增說明文件，沒有調整韌體、校正值或舊文件。

| 主題 | 最終版實際行為 | 查核位置 |
|---|---|---|
| 活躍導航核心 | ESP32 與 old 共用 `navigation_core`；不是直接編譯 old 內原始 Program00 | [platformio.ini](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/esp32_controller/platformio.ini)、[核心入口](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/program00.cpp) |
| 迷宮 | 實體 16×16，展開矩陣 33×33，256 格，A* 有 1024 個格位／朝向狀態 | `NAV_MAZE_SIZE=16`、[maze_dfs.h](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/maze_dfs.h) |
| 起點 | 展開座標 `(1,31)`，目前建置朝南；編譯設定也支援朝西 | `NAV_START_HEADING=2` |
| 任務選擇 | 開機讀正式 Maze Store，有有效 goal route 就 Solve，沒有就 Explore；讀取錯誤則鎖定故障 | [prepareAutomaticMode](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/esp32_controller/src/control/mission_coordinator.cpp#L222) |
| GPIO2 | 活躍 main／link task／coordinator 沒有呼叫實體 selector 更新。proxy 雖保留，不能因此認為仍以開關選模式 | [main.cpp](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/esp32_controller/src/main.cpp)、`mode_selector_proxy.cpp` |
| Explore 結束 | 第一個中央目標格到達名義格心、已知路徑驗證及停止確認成功，就儲存並準備 Solve；無須先完整繞回根節點 | [DFS 包裝](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/modules/program00_navigation.h#L221)、[格心後結束](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/modules/program00_navigation.h#L462) |
| Solve 路線 | 已確認地圖上的最短已知路徑；未探索區域可能仍有更短路，不等於全迷宮最短路證明 | `MazeSolver::solve`、`exportGoalRouteSnapshot` |
| 直行 PWM | normal／slow 都是 50；兩個策略名稱仍分開 | [預設參數](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/config/program00_config.h) |
| 格距 | min／max／nominal／tolerance＝44／48／46／8 counts | 同上 |
| 前方門檻 | Wall 110、slow 250、clear 250、turn 90、around 90 mm | 同上 |
| 從板直行加速 | 每有效控制 frame 增加 6 PWM；其他非零變化最大 64；零輸出立即 | [從板常數與輸出](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/atmega_motor_slave/src/main.cpp#L13) |
| 中斷探索回退 | 17 份 checkpoint，預設回退 8 格，參數 ID73 | 核心預設、`program00ResumeSnapshotForStop` |

表內參數是開機預設，成功的 USB SRAM 設定可覆蓋執行期值，重開機後恢復預設。純編譯旗標不屬於 SRAM 參數。

## 2. 整台車有哪些執行單位

```mermaid
flowchart TB
  subgraph C3["ESP32-C3：單核心，工作交錯排程"]
    MAIN["Arduino loop：約每次 delay 1 ms<br/>USB → MissionCoordinator → 除錯"]
    NAV["runProgram00<br/>其中 checkTimeFlag 每滿 10 ms 更新導航"]
    TURN["阻塞式轉彎／倒退／等待套用<br/>目前佔住主程式呼叫鏈"]
    LINK["motor_link FreeRTOS task<br/>priority 4，堆疊參數 4096<br/>每 5 ms 收 status、送 control"]
    ISR["Encoder／Echo ISR<br/>只記錄邊緣、計數、時間"]
    BOX["受 critical section 保護<br/>最新期望 PWM／motion／revision<br/>最新從板 status"]
    MAIN --> NAV
    NAV --> TURN
    NAV --> BOX
    TURN --> BOX
    BOX <--> LINK
    ISR --> NAV
  end
  SLAVE["ATmega328P 獨立 loop<br/>D13 → UART驗證 → 輸出 → watchdog"]
  LINK <-->|"57600 baud<br/>12-byte control／14-byte status，CRC8"| SLAVE
  SLAVE --> MOT["實際馬達腳位與 PWM"]
```

**只有 UART 在這裡是另外建立的 FreeRTOS task。** `program00_lifecycle.h`、`observation.h`、`motion.h`、`turn.h`、`navigation.h` 是同一個 translation unit 內的子程式，呼叫時依序執行。不能把它們當成五條已平行運作的執行緒。ESP32-C3 也不會讓導航與 UART 同時在兩個 CPU 核心上執行。

| 執行單位 | 何時執行 | 寫入權限 | 重構時要保留的界線 |
|---|---|---|---|
| Encoder ISR | AB 腳位 CHANGE | 原始編碼器計數與前次 AB 狀態 | 不決定格位，不印 log，不控制馬達 |
| Echo ISR | ECHO CHANGE | 上升時間、脈衝長度、完成旗標 | 不做 DFS、不呼叫 I2C；小於 100 μs 的脈衝拒絕 |
| 主 loop | coordinator 呼叫期間 | 任務、導航、觀測、地圖、姿態 | 地圖與姿態由同一擁有者更新 |
| UART task | `vTaskDelayUntil`，5 ms | 封包、連線狀態、傳送／狀態 revision、安全輸出閘門 | 不更新 DFS、格位、感測視窗 |
| ATmega loop | 自由迴圈 | D13 授權、握手、watchdog、實際 PWM | 通訊中斷仍自行關閉輸出 |

來源：[linkTask／motorLinkBegin](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/esp32_controller/src/control/motor_link_client.cpp#L273)、[Encoder ISR](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/esp32_controller/src/sensors/encoder.cpp)、[Echo ISR](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/esp32_controller/src/sensors/ultrasonic.cpp)。5 ms 是排程目標，不是已量測的硬即時保證。

## 3. 從開機到兩趟任務

```mermaid
flowchart TD
  BOOT["setup<br/>USB／ADC／EEPROM相容層／參數／DFS／UART"] --> SAFE["BOOT_SAFE_STOP<br/>輸出閘門關閉"]
  SAFE --> LOAD{"正式 Maze Store<br/>讀取結果？"}
  LOAD -->|"沒有有效記錄"| ER["EXPLORE_READY<br/>要求邏輯 STOP，等待新的 D13 RUN"]
  LOAD -->|"有效記錄"| SL["SOLVE_LOADING<br/>讀圖、A*規劃、verifyPlan"]
  LOAD -->|"儲存系統讀取失敗"| FL["FAULT_LATCHED<br/>持續零輸出"]
  ER -->|"新鮮 STOP→RUN 與 ARM 都通過"| E["EXPLORE_RUNNING<br/>載入 Resume 或空白 DFS"]
  E -->|"第一個 goal 格心提交、路徑合格"| TS["TRANSITION_STOPPING<br/>等待停止要求之後的新 status＝0/0"]
  E -->|"操作者 D13 STOP"| TS
  E -->|"通訊失效"| LR["LINK_RECOVERING<br/>保留本趟 RAM，關閉輸出"]
  LR -->|"新鮮 STOP→RUN＋握手成功"| RES["回到原 Explore／Solve<br/>保持原導航狀態"]
  TS -->|"新鮮零輸出證明"| SAVE["SAVING<br/>Maze／Resume／run log 的待存項目"]
  SAVE -->|"Explore goal route 儲存成功"| SL
  SAVE -->|"Explore 人工中斷且保存成功"| ER
  SAVE -->|"寫入或導航失敗"| FL
  SL -->|"地圖與路線有效"| SR["SOLVE_READY<br/>車放回起點，等待新 D13 RUN"]
  SL -->|"不合格"| FL
  SR -->|"新鮮授權與 ARM 成立"| S["SOLVE_RUNNING<br/>沿唯讀路線執行"]
  SR -->|"SRAM設定 revision 改變"| SL
  S -->|"通訊失效"| LR
  S -->|"goal 格心已提交／人工 STOP／fault"| TS
  SAVE -->|"Solve 成功完成"| SD["SOLVE_DONE<br/>零輸出，等待清圖或重開機"]
  SAVE -->|"其他非fault停止"| SAFE
```

圖中的恢復節點依原任務回到 E 或 S。開始第二趟必須人工把車放回 `(1,31)` 並朝南，再按 D13。韌體不能確認絕對實體姿態。LinkRecovering 則保存中斷當下的 RAM 流程；不要把它誤用成「已搬回起點」的續跑。

`Saving` 目前是同步函式內的短暫狀態。`TransitionStopping` 沒有因等待超時而自動允許寫 Flash 的後門；證據不足會繼續停住。Solve 人工停止後會重新經過自動任務選擇，不一定進 SolveDone。

來源：[missionCoordinatorUpdate](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/esp32_controller/src/control/mission_coordinator.cpp#L583)、[finishPersistence](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/esp32_controller/src/control/mission_coordinator.cpp#L458)。

## 4. 主 loop 與 10 ms tick 的真正邊界

### 4.1 外層呼叫順序

```mermaid
flowchart TD
  A["Arduino loop"] --> B["serviceUsb<br/>停止時設定／讀log／清圖"]
  B --> C["missionCoordinatorUpdate"]
  C --> D{"任務正在行駛且授權有效？"}
  D -->|"是"| P["runProgram00"]
  D -->|"否"| O["處理準備、停止、存檔或恢復狀態"]
  P --> X["mazeMatrixDebugUpdate"]
  O --> X
  X --> Y["delay 1 ms，讓出排程"]
  Y --> A
```

`runProgram00()` 第一次執行還會等啟動延遲 3000 ms、確認授權、初始化超音波／encoder、匯入 Resume、建立直線 segment、校正 MPU。IMU required 而校正失敗時直接停止。啟動延遲與校正同樣會佔住主呼叫鏈。

正常進入後，先做 `ultrasonicUpdate()`，再做 `checkTimeFlag()`。超音波驅動因此靠主程式頻繁服務，ISR 只先留下結果；它沒有自己的 FreeRTOS task。

### 4.2 每滿 10 ms 才做的事

下面依 `checkTimeFlag()` 的實際順序畫出，不能任意交換觀測、決策與提交順序。

```mermaid
flowchart TD
  A{"now-lastTick ≥ 10 ms？"} -->|"否"| R["離開 checkTimeFlag<br/>外層仍可處理決策／轉彎／PWM"]
  A -->|"是"| B["lastTick＝now<br/>漏掉的tick丟棄，不追趕補跑"]
  B --> C["更新 straight／turn drift 診斷<br/>gyro讀取共用，部分等待也服務yaw"]
  C --> D["左右 IR 各讀一次<br/>12-bit ADC縮成10-bit尺度"]
  D --> E["updateIrSteeringControl<br/>100 Hz IIR，計算修正／slew"]
  E --> F["消費一筆新 Echo<br/>更新距離、連續wall／non-wall計數、速度策略"]
  F --> G["消費 timeout<br/>交給超音波安全策略"]
  G --> H["updateCellObservation<br/>同一對IR加入5點去極值視窗"]
  H --> I["updateTurnExitIrReanchor<br/>轉後先找出口邊緣，暫停格位推進"]
  I --> J["有側邊edge候選就維持slow策略"]
  J --> K["updatePostTurnFrontGuard<br/>轉後新鮮前方證據與停止保護"]
  K --> L["updateStraightSegmentTracking<br/>進度→決策快照／hold／格心commit"]
  L --> M["updateStraightStallWatchdog<br/>stall／有界recovery"]
  M --> N["printDebugMonitor<br/>受編譯開關與節流限制"]
```

來源：[checkTimeFlag，1017 行起](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/modules/program00_lifecycle.h#L1017)。時間判定是 `if`，沒有 `while` 補跑，也沒有 `lastTick += 10`。如果阻塞 300 ms，返回後只處理一次現在的樣本，不能把現在同一讀值算成 30 筆歷史觀測。

### 4.3 tick 返回以後，仍在同一次 runProgram00 內

```mermaid
flowchart TD
  A["checkTimeFlag 返回"] --> B{"CallDfs？"}
  B -->|"是"| C["先清 CallDfs<br/>讀固定的 decisionCell／F L R／有效位元"]
  C --> D["Explore：DFS／暫定側向probe<br/>Solve：取預先驗證路線下一步"]
  D --> E["Continue→保存pendingHeading與turn reference<br/>GoalReached→等待格心完成<br/>錯誤→停止"]
  B -->|"否"| F["檢查尚未執行的回溯<br/>Explore可用新證據取消回溯"]
  E --> F
  F --> G["handlePendingTurn<br/>先定位；條件達成才執行阻塞式轉彎"]
  G --> H{"有任一有效hold？"}
  H -->|"是"| Z["stopMotors<br/>邏輯isMotorRunning清除"]
  H -->|"否"| I["必要時motorStart<br/>centerCar或不修正直行，更新期望PWM"]
  Z --> R["返回coordinator"]
  I --> R
```

hold 來源包含待轉點、回溯重測、側開口確認、倒退後等 Echo、轉後前方 guard。它們最後共同決定是否保持零輸出。完整轉彎鏈與六個 FuncFlags 的設立／解除位置見附篇。

## 5. 觀測怎麼變成可用的地圖證據

### 5.1 同一組 IR，有兩個不同速度的消費者

```mermaid
flowchart LR
  RAW["每10 ms的左右raw pair"] --> ST["轉向修正用IIR<br/>至少3筆才可影響PWM<br/>deadband／divisor／限幅／slew"]
  RAW --> WIN["地圖用5筆視窗<br/>刪最小、最大，平均其餘3筆"]
  WIN --> CLASS["Wall／Open遲滯<br/>多視窗確認＋pair／位移證據檢查"]
  CLASS --> EDGE["Wall→Open先記候選edge<br/>edge＋offset處停止後確認"]
  CLASS --> VALID["各側 observation validity<br/>拒絕資料可阻擋當次未知支路<br/>不可直接永久寫成Wall"]
  ST --> PWM["期望左右PWM"]
  EDGE --> OBS["固定本次decision observation"]
  VALID --> OBS
  OBS --> PLAN["DFS或Solve route檢查"]
```

「IR 居中修正」指沿走廊調整左右速度，`isNeedToCenter` 不是「已到格心」。格心由 encoder 與 segment commit 管理。

Wall／Open 視窗使用調整後 IR 尺度，預設 offset 左右各 400、Open threshold 550、Wall 遲滯界線 400。pair minimum sum 預設 0 只停用額外總和門檻，其他位移與共用證據保護仍存在。無走廊參考值時，不能憑「參考不存在」就封死側路。

### 5.2 Front 的三種含義不能混用

一般觀測以連續兩筆 `<110 mm` 當 Wall，連續兩筆非 Wall 更新 non-wall 狀態。這裡的 non-wall 不代表一定有 250 mm 空間。Solve 對 saved Wall 的矛盾判定另要求 fresh Echo `>=250 mm`；110～249 mm 不足以宣告保存地圖錯誤。普通轉彎與死路 Around 的前牆定位目前都用 90 mm。

超音波 nominal trigger interval 為 20 ms，active measurement timeout 為 30 ms。10 ms tick 只是取用新結果，不會每次都產生新 Echo。沒有 Echo 是 timeout／無證據事件，不可當作 Clear，也不能拿轉前殘留距離當轉後結果。

### 5.3 為何有停止後多視窗重測

Explore 要回溯、感測與既有拓撲衝突時，會先停住、重取 F/L/R，通常確認三輪後才決定。已知 Wall 的修復只能在停止且新證據確認後修改內部 passage，不能改迷宮邊界或柱子。Solve 的 snapshot 唯讀，矛盾持續就停止，不在行進中修圖或重新跑 A*。

來源：[觀測模組](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/modules/program00_observation.h)、[tracking／重測](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/modules/program00_motion.h#L2829)、[Solve observation policy](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/solve_observation_policy.h)。

## 6. 每格的決策與格心提交是兩件事

```mermaid
sequenceDiagram
  participant S as Segment tracking
  participant O as Cell observation
  participant N as Navigation decision
  participant P as Committed pose
  S->>O: encoder到決策區，檢查F/L/R完整性
  O-->>S: 固定decision cell、牆與有效位元
  S->>S: dfsTriggeredForNextCell=true，CallDfs=true
  S-->>N: tick返回，外層消費CallDfs並清除
  N-->>S: 下一絕對方向、pending turn或completion pending
  Note over S,P: 尚未滿足格心commit時，不宣告車體已進入該格
  S->>P: 名義格心到達，且CallDfs已清除
  P->>P: markTraversed、carX/Y、committedCells更新
  P->>P: checkpoint或Solve route index提交
  P-->>N: 完成條件可在此確認
```

標準直線第 n 格：`nextCellCenter = n × 46`，決策邊界通常為 `nextCellCenter - 8`，提交點為 `nextCellCenter`。第一格使用起跑格心的特殊初始化，側邊 edge、前牆及轉後 guard 也有各自定位條件，不能把所有情況都簡化成固定 38 counts 呼叫 DFS。

`updateStraightSegmentTracking()` 先排除 false-turn recovery、recovery crawl、re-anchor、倒退後等待等擁有者，才可推進格位。普通提交要求 `dfsTriggeredForNextCell && !CallDfs`；剛產生決策事件的 tick 不會同時跨過尚未處理的決策去提交。

DFS 的內部 map／path 可能先隨決策更新；`carX/Y` 的實體格心承諾較晚。因此錯誤轉彎才需要 transaction 紀錄來源格、舊方向與 stack 變化，並在物理撤回後原子恢復。重構時若直接在 DFS 回傳時更新實體 pose，會讓 rollback 失去來源。

另有轉彎提交路徑：成功到達所選轉向參考點並完成轉向後，`handlePendingTurn()` 會提交 decision cell 與新 heading；它可能早於普通 encoder 名義格心提交。重構必須保留這個獨立、受定位條件約束的提交入口，不能只留下直行 commit。

直線 encoder progress 從上次建立 segment 時累積，多格直行靠 `committedCells` 算下一格，不在每格把 encoder 歸零。來源：[tracking 2829、3385 行起](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/modules/program00_motion.h#L3385)。

## 7. 路徑規劃怎麼接到動作

| 層 | 輸入 | 輸出 | 不應負責的工作 |
|---|---|---|---|
| Explore DFS | decision cell、絕對朝向、F/L/R、各側有效位元 | Continue／GoalReached／Complete／錯誤、絕對下一方向、是否回溯 | 不直接寫馬達PWM |
| Known-route／return planner | 已確認 Open／Visited 圖與可信現場限制 | 通往 frontier 的下一步或回程路徑重建 | 不穿越 Unknown |
| Solve A* | 停車時載入的不可變 snapshot | headings 路線、moves、quarter turns、goal | 不在每10 ms重新規劃 |
| 動作協調 | `nextHeading` 與目前 `carHeading` | 直行／右90／180／左90、待轉定位 | 不因期望方向已決定就提前改heading |
| Turn executor | 定位完成、感測與馬達授權 | 成功／失敗、轉後驗證狀態 | 不把encoder telemetry當IMU成功 |

方向編碼 N/E/S/W＝0/1/2/3；`relative = (nextHeading + 4 - currentHeading) & 3`，結果 0＝直行、1＝右、2＝迴轉、3＝左。

Explore 預設普通支路順序是前、左、右，直接可見中央 goal 另有優先規則。被 quarantine 的 Unknown 中央目標側路也可能先走 tentative probe：先暫定 stack、轉向、停止重取 front Echo；確認 Clear 才提交 Open，Wall／timeout 則轉回取消。已知 Wall 不能被這個優先規則繞過。

Solve A* 以格數為第一成本、90° quarter turns 為第二成本。執行時 `nextNavigationDecision` 確認候選格位、到達朝向及 route passage；側 IR 保留 steering／診斷，但不能自行否定已驗證的 A* 側向 Open。轉後由新 front Echo guard 查該通道。每格 route index 要等格心提交才推進。

來源：[nextNavigationDecision](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/modules/program00_navigation.h#L325)、[maze_solver.cpp](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/maze_solver.cpp)、[maze_dfs.cpp](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/maze_dfs.cpp)。

## 8. UART 與安全輸出的狀態

### 8.1 期望值到實際輸出

```mermaid
sequenceDiagram
  participant NAV as Navigation / Turn
  participant MB as Command mailbox
  participant UART as UART task 5 ms
  participant AVR as ATmega
  NAV->>MB: desired PWM、motion、命令revision加一
  UART->>MB: 讀取一致的命令與安全gate
  UART->>AVR: CRC8 control frame
  UART->>MB: transmittedDesiredRevision與frame count
  AVR->>AVR: CRC／session／sequence／D13／ARM檢查
  AVR->>AVR: 套用PWM或立即停止
  AVR-->>UART: applied PWM／flags／state
  UART->>MB: status revision加一，更新最新回報
  NAV->>MB: 等待要求之後的新證據
```

「已設定 desired」只代表主程式要求；「revision transmitted」只代表 UART task 已送出；「applied PWM」才是從板回報其輸出，仍不等於車身確實移動。移動要由 encoder／IMU等感測另證明。

### 8.2 連線與握手

```mermaid
stateDiagram-v2
  [*] --> Disarmed
  Disarmed --> LocalStop: D13 STOP
  LocalStop --> Disarmed: D13新按下切RUN
  Disarmed --> Syncing: 合法HELLO與零PWM，新session
  Syncing --> Armed: 至少4個連續零frame、有效mode、ARM
  Armed --> Armed: 同session有效motion／冪等握手尾包
  Armed --> LocalStop: D13 STOP或同session bare HELLO
  Armed --> Lost: 控制watchdog／新session等失效
  Syncing --> Lost: 控制watchdog等失效
  Lost --> LocalStop: 實體D13先切STOP
```

ESP32 先發 HELLO 零 frame，第 5 個起可帶 ARM；從板自己還要求 4 個連續零 frame。ESP32 的條件不能取代從板檢查。Lost 不會靠新 HELLO 自動恢復，需要 D13 STOP 再 RUN。

### 8.3 人看得懂的通訊旗標

| 狀態／欄位 | 意義 | 單獨成立夠不夠行駛 |
|---|---|---|
| `missionModeValid` | 主控已決定 Explore／Solve | 不夠 |
| `missionOutputAllowed` | coordinator 允許傳輸非零任務輸出 | 不夠 |
| `remoteLocalRunAllowed` | 從板 D13 邏輯狀態為 RUN | 不夠 |
| `remoteArmed` | 相同 session，從板完成握手 | 不夠 |
| `runResetRequired` | 已失效，仍欠新的操作員復原授權 | true 時禁止 |
| `controllerFault` | 主控宣告故障 | true 時禁止 |
| `StatusModeValid／ModeSolve` | 從板接受的任務型別 | 須符合主控期望 |
| `statusRevision` | 每收到一筆有效status增加的版本號 | 用來區分新舊證據 |
| `desiredRevision` | 每次寫入期望命令的版本號 | 用來識別要等待哪個命令 |

完整行駛資格由 `motorLinkNavigationRunAllowed()` 統一 AND 判斷。不要在其他模組自行用「有 ARM」或「按了 D13」取代它。

Control bits：bit0 ARM、bit1 STOP、bit2 Straight、bit3 Turn、bit4 Reverse、bit5 Fault、bit6 HELLO、bit7 Brake。HELLO 零PWM握手期間，STOP 單獨作 Explore marker、Fault 單獨作 Solve marker；上鎖後恢復安全含義。必須依協定階段解碼。

Status bits：bit0 LocalRun、bit1 OutputZero、bit2 WatchdogReset、bit3 BrakeApplied、bit4 ModeValid、bit5 ModeSolve。完整列舉見 [motor_link_protocol.h](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/shared/motor_link_protocol.h)。

### 8.4 四種時間不能合成一個timeout

| 時間 | 現行數值／來源 | 用途 |
|---|---|---|
| 控制送出 | 5 ms | 最新命令更新 |
| 從板控制watchdog | 直行50 ms，轉向／倒車20 ms | 有效control消失時切斷輸出 |
| 週期status | 每6個CRC有效control，名義約30 ms | 狀態回傳；轉向輸出／停止等重要事件可立即回報 |
| 主控 running status freshness | 2000 ms | 允許短暫回傳丟失；不是馬達watchdog放寬 |
| coordinator 停止／存檔證據 | 150 ms內，且revision較停止要求新 | 安全存檔與授權 |
| AVR硬體watchdog | 120 ms | 從板自身loop卡死復原 |

從板每次 loop 是 `updateRunButton → serviceSerial → checkLinkTimeout → wdt_reset`。CRC錯誤、重複序號、錯誤session等拒絕封包不能冒充有效控制來延命。不同故障bit可能只是診斷，具體能否動作還要看 link state，不能把任何非零faultFlags都直接等同同一種停車。

## 9. recovery 的擁有者要分開

| 機制 | 觸發問題 | 主要流程 | 地圖／姿態權限 |
|---|---|---|---|
| ordinary stall | 期望前進卻缺乏可信encoder進度 | 停→有界倒退→IR或合格IMU修正→停→fresh F/L/R→慢爬／再試／停 | 動作本身不能虛構格位；Explore fresh junction／dead-end結果可交回正常DFS |
| false-turn transaction | 90°轉後立即遇牆，仍在來源格可撤回範圍 | 分段倒退→停測→反向轉回→原子恢復DFS→來源重決策 | 只在transaction與物理撤回都通過時恢復；已穿越Open不可隨意封Wall |
| tentative side probe | Unknown側路證據不夠，但合乎probe條件 | 暫定選路→90°→停測front→提交Open或轉回取消 | 未確認前保持Unknown |
| no-Echo recovery | timeout且符合當前owner安全條件 | 有界慢退／停止／等待新Echo | timeout不是牆也不是Clear |
| post-turn front guard | 轉後資料未準備好或fresh near Wall | 停車或有界慢爬，交給適當recovery | 不得在零進度製造下一格 |
| link recovery | status失效、ARM或session異常 | gate關閉，等待fresh人工授權與握手 | 保存目前RAM任務，不自動回起點、不重設DFS |

重構時應讓每個 recovery 都有 entry、deadline、退出原因與動作擁有權。事件同時成立時，先由單一協調者選 owner；不要讓 stall、front guard、false-turn 各自把相同PWM改掉。

## 10. 建議你重構的架構（以下是新設計建議）

### 10.1 先保留一個導航擁有者

建議保留目前獨立 UART transport task，把導航改成非阻塞狀態機。感測、地圖、路線與動作不必各開一條執行緒。先把責任與資料邊界切清楚，才決定是否真的需要額外 task。

```mermaid
flowchart TD
  SENSOR["SensorService<br/>ISR結果→有revision與時間戳的樣本"] --> OBS["ObservationBuilder<br/>SteeringEvidence／TopologyEvidence"]
  OBS --> POSE["SegmentTracker<br/>encoder進度／candidate cell／committed pose"]
  POSE --> NAV["NavigationCoordinator<br/>唯一導航狀態與動作仲裁者"]
  NAV --> PLAN["DecisionProvider<br/>Explore DFS或Solve route"]
  PLAN --> NAV
  NAV --> ACT["MotionExecutor<br/>Straight／Turn／Reverse／Hold子狀態機"]
  ACT --> CMD["MotorCommand mailbox<br/>commandId／motion／PWM／有效期限"]
  CMD --> LINK["既有UART task與從板安全鏈"]
  LINK --> ACK["AppliedFeedback<br/>session／revision／時間／PWM"]
  ACK --> ACT
  ACT -->|"Completed／Failed事件"| NAV
  NAV --> STORE["PersistenceService<br/>停車證據通過才提交"]
```

建議模組 API 如下，名稱是規劃示意，尚未實作：

```cpp
SensorFrame sampleSensors(Time now);             // 只收集，不動地圖
void observation.update(const SensorFrame&);     // 保留樣本revision
ProgressEvent tracker.update(const SensorFrame&);
Decision planner.decide(const DecisionContext&); // 回傳值，不寫馬達
void motion.begin(const MotionRequest&);          // 只接一次動作
MotionEvent motion.update(Time now, const SensorFrame&, const MotorFeedback&);
void navigation.update(Time now);                 // 仲裁、處理事件、commit
void persistence.update(const StopProof&);        // stopped-only
```

`sampleSensors` 也可頻繁服務非同步Echo，IR／steering維持10 ms節奏。若要保留最終版的證據時序，不能把每次外層loop都當成新的IR取樣。

### 10.2 主狀態與子狀態

主狀態描述「正在完成什麼」，子狀態描述「這個動作做到哪一步」。用 enum 互斥狀態取代一堆互相矛盾的 active／pending／done 布林值。

| 層級 | 建議狀態 | 完成後交給誰 |
|---|---|---|
| Mission | BootSafe、ExploreReady、ExploreRunning、SolveReady、SolveRunning、Stopping、Saving、LinkRecovering、Done、Fault | 操作員授權與任務政策 |
| Navigation | AcquireObservation、Advance、Decide、ApproachTurnPoint、ExecuteTurn、VerifyExit、Recover、CommitCell、Finish | Mission |
| Motion | Idle、AwaitStartAck、Driving、AwaitStopAck、Settling、Completed、Failed | Navigation |
| Turn | EntryStop、EntryClearance、MainLeg、LegStop、FineCorrection、LegAccepted、NextLeg、ExitAnchor | Motion／Navigation |
| Recovery | Stop、Reverse、Recheck、Correct、Crawl、UndoTransaction、Failed | Navigation |

```mermaid
stateDiagram-v2
  [*] --> AcquireObservation
  AcquireObservation --> Advance: 證據合格且路徑安全
  Advance --> Decide: 到達決策位置，固定context
  Decide --> Advance: 下一方向相同
  Decide --> ApproachTurnPoint: 需要轉彎
  ApproachTurnPoint --> ExecuteTurn: 定位完成與授權成立
  ExecuteTurn --> VerifyExit: 每腿驗收成功
  VerifyExit --> Advance: 新證據與reanchor合格
  Advance --> CommitCell: 到名義格心且決策已消費
  CommitCell --> AcquireObservation: 不是終點
  CommitCell --> Finish: 終點與停止證據合格
  Advance --> Recover: stall或合格guard事件
  VerifyExit --> Recover: 可撤回的錯誤通道
  Recover --> AcquireObservation: 恢復成功
  Recover --> Fault: 證據不足或有界動作失敗
  ExecuteTurn --> Fault: IMU／輸出／encoder失敗
```

上圖是新架構的語意示意，不要求第一版就搬動所有邏輯。現行程式有分支把定位、觀測與格心commit交錯處理；移植時保留原條件與先後，再逐步整理。

### 10.3 把阻塞式轉彎改成 update

每次 `update()` 只做當前狀態的短工作，再立刻返回。等待時儲存 `enteredAt`、`deadline`、`requestedCommandId`、`baselineStatusRevision`、`encoderOrigin`、`targetHeading`。等待零輸出時只在 entry 發一次 stop，不每次update重建命令版本。

細節見轉彎附篇。IMU角度合格、encoder沒有過量或無動作故障、套用／停止確認成功後，才產生 `TurnCompleted`，由導航唯一更新 `carHeading`。180°用兩個90°腿，每腿各自驗收。失敗只發一次事件，保持零輸出，不跳回正常直行。

### 10.4 旗標改成事件、狀態與證據

| 現行概念 | 重構建議 | 理由 |
|---|---|---|
| `CallDfs` | 帶 `decisionId` 的 `DecisionReady` 事件 | 只處理一次，攜帶固定觀測，不被後續取樣改內容 |
| `isMotorRunning` | `motion.state` 與 `MotorFeedback` 分開 | 邏輯想跑與從板真正輸出不同 |
| `NeedToSlowDown` | 由有效原因推導 `SpeedPolicy` | front、side edge、post-turn、recovery不能互相錯誤清旗標 |
| 多個 hold bool | `HoldReason` 集合＋唯一動作仲裁 | 顯示誰持有停止權，所有原因解除才能出發 |
| `hasPendingTurn` | `MotionRequest`＋狀態ApproachTurnPoint | 保存目標、參考點與transaction，避免只剩true卻不知道要做什麼 |
| `isDFSFinished` | 結束事件帶 `Success／Fault／Stopped` 原因 | 結束不必然是成功 |
| observation Ready | `Evidence` 含有效性、時間、revision、sensor source | fresh／stale／Unknown可被明確判斷 |
| `explorationQualified` | 驗證結果＋snapshot id | 明確區分可Solve與全圖complete |

### 10.5 動作優先權

建議由高到低：D13／從板watchdog／session失效 → 輸出故障 → 已取得ownership的recovery → 轉彎／定位 → 等觀測hold → 正常直行。每次仲裁只產生一份命令，不讓感測模組直接寫PWM。從板保留最後的獨立安全權限。

### 10.6 可分批重構的順序

1. 先把 SensorFrame、MotorFeedback、CommittedPose、DecisionContext 的型別固定，保留原演算法。
2. 將輸出集中在唯一 MotionExecutor，先加命令／狀態revision，不改校正參數。
3. 把 stop／brake／reverse 的阻塞等待拆成子狀態，驗證新舊完成與失敗事件一致。
4. 拆90°轉彎，再用兩腿組合180°，保留IMU與encoder各自角色。
5. 將CallDfs轉成一次性事件，保留decision→格心commit順序與rollback交易。
6. 最後拆recovery、NVS提交與任務政策，移除不再需要的布林旗標。

可用確定性的感測／status回放測狀態轉移，至少涵蓋：正常直行多格、左右轉、180°、轉後舊Echo、套用ack延遲、D13停止、link中斷復原、stall、假側路撤回、決策點早於格心、goal到格心才完成、存檔失敗保留舊槽。實車驗證仍另行完成，host測試不能證明物理角度或里程校正。

## 11. 原始碼閱讀路線

| 順序 | 檔案 | 先找的函式／資料 |
|---|---|---|
| 1 | [ESP32 main](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/esp32_controller/src/main.cpp) | setup、loop |
| 2 | [任務協調](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/esp32_controller/src/control/mission_coordinator.cpp) | prepareAutomaticMode、missionCoordinatorUpdate、beginStopTransaction |
| 3 | [核心宣告](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/program00.cpp) | ProgramMissionRuntime、CellObservation、StraightSegmentTracking、NavigationState |
| 4 | [生命週期](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/modules/program00_lifecycle.h) | resetProgram00、runProgram00、checkTimeFlag |
| 5 | [觀測](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/modules/program00_observation.h) | updateIrSteeringControl、updateCellObservation、cellObservationReady |
| 6 | [移動與格位](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/modules/program00_motion.h) | updateStraightSegmentTracking、updateStraightStallWatchdog |
| 7 | [轉彎](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/modules/program00_turn.h) | handlePendingTurn、turn90EncoderBlocking |
| 8 | [決策介面](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/modules/program00_navigation.h) | nextNavigationDecision、commitDecisionCellForMission、finishMissionAfterCommittedCell |
| 9 | [DFS](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/maze_dfs.cpp)、[A*](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/navigation_core/src/navigation_core/maze_solver.cpp) | step、solve、verifyPlan |
| 10 | [motor proxy](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/esp32_controller/src/control/motor_proxy.cpp)、[UART client](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/esp32_controller/src/control/motor_link_client.cpp) | desired、transmitted、applied三階段 |
| 11 | [從板](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/atmega_motor_slave/src/main.cpp) | handleSyncing、handleArmed、checkLinkTimeout |
| 12 | [Maze Store](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/esp32_controller/src/persistence/maze_store.cpp)、[Resume Store](https://github.com/ccccchhhheeenng/mouse_maze_solver/blob/6b0b32cd013409a328a92c89b0d348b7e864337c/esp32_controller/src/persistence/resume_store.cpp) | A/B槽、CRC、commit與回讀 |

本次為程式閱讀與文件整理，未修改 C++，未建置、燒錄、開啟序列埠或做實車測試。文件驗證以來源追蹤、連結與流程圖檢查為範圍。
