123 lines
8.6 KiB
Markdown
123 lines
8.6 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 hai
|
||
framework kia**. Lớp nối (`MissionAdapterBridge`, `RecoveryRunner`) được thêm ở bước sau, và chúng
|
||
mới là chỗ duy nhất được phép include.
|
||
|
||
Kiểm bằng:
|
||
|
||
```bash
|
||
grep -rn "mission_adapters\|recovery_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 thành một.** `moveTo` ×2, `dockTo` ×2, `moveStraightTo`, `rotateTo` của
|
||
contract host chỉ khác nhau ở kiểu chuyển động và sai số mặc định. Bảng `ProfileBinding` mô tả đúng
|
||
phần khác nhau đó; sáu hàm còn lại chỉ dựng struct rồi gọi một đường vào duy nhất.
|
||
|
||
**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`.
|
||
|
||
Chưa có, thuộc bước nối dây runtime:
|
||
|
||
- Hiện thực thật của `PlannerPort` / `ControllerPort` / `PosePort`
|
||
(bọc costmap, boost::dll, TF).
|
||
- Dựng hai `Costmap2DROBOT` thật. Hiện `NavigationServer::attachCostmaps()` nhận `LayeredCostmap*`
|
||
từ bên ngoài bơm vào — cố ý, để đường cảm biến kiểm được mà không cần TF và cây config thật.
|
||
- Thread planner riêng và bộ đệm plan ba lớp. Hiện `ControlLoop` lập plan đồng bộ ngay trong cycle;
|
||
tách như vậy để phần quyết định kiểm được mà không cần thread.
|
||
- Lớp nối tới mission framework.
|
||
- `setTwistLinear` / `setTwistAngular` (hiện trả `false` để host biết lệnh không có hiệu lực, thay vì
|
||
âm thầm bỏ qua).
|