139 lines
9.9 KiB
Markdown
139 lines
9.9 KiB
Markdown
# 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.
|