Files
move_base2/docs/ARCHITECTURE.md
2026-08-03 22:41:32 +07:00

139 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Kiến trúc
## Ba lớp
```
┌──────────────────────────────────────────────────────────────────────┐
│ Biên (adapter) │
│ │
│ NavigationServer ──implements──▶ move_base_core::BaseNavigation │
│ · quy 6 entry point di chuyển về 1 NavigationRequest │
│ · nhận dữ liệu sensor từ host │
│ · kết xuất trạng thái ra kiểu mà host mong đợi │
└──────────────────────────────────┬───────────────────────────────────┘
┌──────────────────────────────────▼───────────────────────────────────┐
│ Điều phối │
│ │
│ ControlLoop — một control cycle: gom dữ liệu, chạy state machine, │
│ thi hành output, phát lệnh vận tốc │
└──────────────────────────────────┬───────────────────────────────────┘
┌──────────────────────────────────▼───────────────────────────────────┐
│ Lõi quyết định (logic thuần, không I/O) │
│ │
│ StateMachine — bảng chuyển state │
│ VelocityArbiter — ai được phát lệnh, và lệnh đó có an toàn không │
│ NavigationRequest — kiểu dữ liệu duy nhất đi vào lõi │
└──────────────────────────────────────────────────────────────────────┘
▼ (qua port, không qua kiểu cụ thể)
ClockPort PosePort PlannerPort ControllerPort
RecoveryPort MissionPort ActionPort
```
Lõi không đụng costmap, không đụng TF, không log, không cấp phát trong vòng lặp. Vì vậy nó kiểm được
bằng bảng thay vì phải dựng runtime hay chạy robot.
## Chiều phụ thuộc
```
move_base2 ──▶ mission framework
──▶ recovery framework
```
**Một chiều, không có đường ngược.** Hai framework kia không biết `move_base2` tồn tại và không biết
lẫn nhau. Hệ quả có thật, không phải hình thức:
- Ba gói test được độc lập.
- Không có vòng phụ thuộc trong build.
- `move_base2` không bị khoá cứng vào một hiện thực mission hay recovery cụ thể — đổi framework chỉ
cần viết lại lớp nối, không phải sửa lõi.
Ở Phase 1, chiều này còn được giữ ở mức mạnh hơn: **không file nào trong `move_base2` include ba
framework kia**. Lớp nối được thêm ở bước sau và chúng mới là chỗ duy nhất được phép include:
`RecoveryRunner` cho `recovery_core`; `ActionRunner` cho `action_core`;
`bridges/mission_adapter_bridge` (dịch contract) và `bridges/mission_layer` (lắp ráp registry +
hàng đợi + hai thread) cho `mission_adapters`.
Kiểm bằng:
```bash
grep -rn "mission_adapters\|recovery_core\|action_core" src/AMR_T800/Test/move_base2/include \
src/AMR_T800/Test/move_base2/src
```
## Vì sao là port chứ không phải gọi thẳng
Bảy port đều nhỏ và đều tồn tại vì một lý do vận hành cụ thể:
| Port | Lý do tồn tại |
|---|---|
| `ClockPort` | Không có nguồn thời gian tiêm được thì không cách nào kiểm hành vi khi control loop chạy chậm hơn chu kỳ cấu hình — đúng lớp lỗi mà dead-reckoning theo chu kỳ danh nghĩa mắc phải |
| `PosePort` | Contract "trả false = dừng an toàn" phải kiểm được mà không cần dựng TF thật |
| `PlannerPort` | Gộp hai overload `makePlan` của interface gốc thành một; "có Order hay không" chỉ là một nhánh nhỏ bên trong |
| `ControllerPort` | Giữ nguyên bộ hàm và **thứ tự gọi** của interface được bọc (hỏi đã tới đích trước, chỉ khi chưa mới tính lệnh) |
| `RecoveryPort` | Chuẩn hoá kết quả tick về ngôn ngữ của `move_base2`, để lõi không phải include recovery framework |
| `MissionPort` | Luồng một chiều có callback, thay cho việc phía mission phải poll trạng thái navigation |
| `ActionPort` | (D8) Runtime điều phối trọn một mission — nav xong chạy nốt action rồi mới báo kết quả. Tick-based cùng nhịp control loop như recovery, và tick của nó **không có** vận tốc: action cần chuyển động phải là motion profile, không phải action |
## Quyết định thiết kế đáng ghi lại
**Sáu entry point gộp về một contract lõi.** `moveTo` ×2, `dockTo` ×2, `moveStraightTo`, `rotateTo`
của contract host chỉ khác nhau ở kiểu chuyển động. `moveTo(PoseStamped)` là direct **position**
goal nên trước tiên đi `MissionLayer::submitGoal()``GoalSourceAdapter``MissionManager`; nhờ đó
nó nhận mission ID và lifecycle/cancel giống VDA5050. Nếu mission layer bị tắt hoặc không nạp source
này mới fallback tương thích về `NavigationRequest` trực tiếp. Các API mang profile/marker riêng
(`dockTo`, `moveStraightTo`, `rotateTo`) dựng request trực tiếp, vì schema `geometry.pose_stamped`
chưa biểu diễn được marker/profile của chúng.
**Một đường vào duy nhất.** `ControlLoop::submit()` là chỗ duy nhất một goal lọt được vào lõi, và
`IDLE → PLANNING` là transition duy nhất bắt đầu một chặng. Nhờ đó việc chống hai nguồn goal tranh
nhau là tính chất cấu trúc, không phải thứ phải nhớ khoá bằng tay.
**Từ chối tại cửa.** Goal có quaternion hỏng, toạ độ không hữu hạn, hoặc profile không nạp được
planner đều bị từ chối ngay trong `submit()` kèm lý do — không để state machine bắt đầu một chặng rồi
mới phát hiện không có planner nào chạy được.
**Phản hồi trễ một cycle.** State machine chạy với phản hồi thu được từ cycle trước, rồi mới gọi
cổng. Điều này là tường minh và có chủ đích: nó cắt vòng "gọi để biết nên gọi gì", nhờ vậy output của
state machine là một danh sách hành động thuần tuý.
**Lệnh 0 là tức thì.** Nguồn `kNone` phát đúng 0, không giảm tốc dần. Lệnh vận tốc bị chốt lại ở tầng
dưới, nên nếu control loop dừng giữa lúc đang giảm tốc thì lệnh khác 0 cuối cùng vẫn còn hiệu lực.
Việc giảm tốc theo động học thuộc về bộ điều khiển bánh xe, nơi biết tải và ma sát thật.
## Trạng thái hiện tại và phần còn thiếu
Đã có và chạy được:
- Toàn bộ lõi quyết định, có test phủ đủ bảng chuyển state.
- `ControlLoop` chạy end-to-end với thành phần giả.
- `NavigationServer` hiện thực đủ contract host.
- `SensorGateway`: dữ liệu cảm biến đi từ contract host tới đúng layer của hai costmap, có phát lại
static map nhận trước khi costmap tồn tại. Đây là file duy nhất trong gói include `robot_costmap_2d`.
- `getTwist()` trả **lệnh** vận tốc từ `VelocityArbiter`, đóng dấu theo đồng hồ của control loop.
- Plugin `libmove_base2.so` export alias `MoveBase2`.
Bước nối dây runtime đã xong (Phase 4):
- `PlannerRunner` / `ControllerRunner` / `RecoveryRunner` / `ActionRunner` nạp plugin thật qua
boost::dll; `CostmapPosePort` lấy pose từ costmap — **hai** instance, khác frame (`map` cho
planner, `odom` cho controller/recovery).
- `NavigationRuntime` dựng hai `Costmap2DROBOT` thật rồi trả về `ControlLoopDeps`.
`NavigationServer::attachCostmaps()` vẫn nhận `LayeredCostmap*` từ ngoài để đường cảm biến kiểm
được mà không cần TF và cây config thật.
- Thread planner riêng + hoán vị ba buffer, `PlannerPort` bất đồng bộ.
- `setTwistLinear` / `setTwistAngular`**trần vận tốc**, không phải lệnh jog.
- Lớp nối tới mission framework: `MissionAdapterBridge` (dịch contract) + `MissionLayer` (dựng
registry, hàng đợi, hai thread). Order VDA5050 vào bằng `moveTo(Order, …)` được cắt thành từng
chặng. Direct position goal vào bằng `moveTo(PoseStamped)` đi `GoalSourceAdapter`, vì vậy cũng có
mission ID thay vì `0`; tắt bằng `mission_layer_enabled: false` thì cả hai loại fallback xuống
đường direct tương thích.
Chưa có:
- Kết xuất lưới costmap cho rviz đã có, nhưng đường `OccupancyGridUpdate` incremental đã bị bỏ —
luôn gửi lưới đầy đủ ở 1 Hz.
- Action chạy **dọc đường đi** (edge action): mọi action hiện chạy sau khi tới goal của chặng.