optimal & fix file cmake

This commit is contained in:
2026-08-03 22:32:40 +07:00
parent 89add78c7f
commit 887bff1b97
98 changed files with 8971 additions and 1339 deletions

View File

@@ -1,46 +1,106 @@
# Kiến Trúc recovery_core
# Kiến trúc recovery_core
## Vị trí trong stack
## Vị trí
`recovery_core` nằm ở tầng `Navigations/Libraries`, cạnh `robot_clear_costmap_recovery`. Nó
giữ vai trò interface tương tự `robot_nav_core::RecoveryBehavior` cho các recovery cần **trả
về output** (path/vận tốc), đồng thời vẫn dùng global path/costmap/tf như bản gốc.
Gói nằm ở `Test/recovery_core`. Nó định nghĩa interface recovery **duy nhất** của workspace
(`CLAUDE.md`); bản legacy `robot_nav_core::RecoveryBehavior` đã dừng phát triển và không được port
sang.
## Các thành phần
Consumer đầu tiên là `RecoveryRunner` của `move_base2` — nó include thẳng
`recovery_core/recovery_behavior.h`, nên contract được compiler kiểm đầy đủ ở một nơi.
- `RecoveryBehavior` (interface, template-method): API non-virtual `configure`/`start`/`update`/
`cancel`; plugin chỉ triển khai hook `onConfigure`/`onStart(goal)`/`onUpdate()`.
- `RecoveryContext`: gói con trỏ ngữ cảnh (tf/costmap/global_path) truyền một lần qua `configure`.
- `RecoveryGoal`: mục tiêu RUNTIME mỗi lượt (angle/distance/target_pose/params).
- `RecoveryResult` / `RecoveryStatus` / `RecoveryOutputType`: hợp đồng output hợp nhất 3 họ +
rich feedback (`progress`/`remaining`/`elapsed`/`message`), thêm trạng thái `kCancelled`.
- Plugin mẫu:
- `ClearCostmapRecovery`: clear layer costmap theo tên, one-shot no-output.
- `RotateRecovery`: quay tới `goal.angle` (rad); sinh `Twist.angular.z` mỗi cycle.
- `BackUpRecovery`: lùi tới `goal.distance` (m); sinh `Twist.linear.x < 0` mỗi cycle.
- `RegenPathRecovery`: trả lại `robot_nav_msgs::Path` từ `global_path` hiện tại.
## Vì sao tick-based
Recovery thế hệ trước chạy blocking bên trong một lời gọi và không trả gì. Hệ quả: không cancel
được giữa chừng, không báo tiến độ, và **không phát được vận tốc** — nên mọi behavior cần robot cử
động phải tự quay vòng lặp bên trong, tranh quyền phát `cmd_vel` với controller.
Bản này trả kết quả từng cycle. Ràng buộc kéo theo: recovery phải được tick từ **đúng thread đang
sở hữu cmd_vel**, không được có thread riêng.
## Thành phần
| Thành phần | Vai trò |
|---|---|
| `RecoveryBehavior` | Base template-method. Giữ toàn bộ bất biến; plugin chỉ triển khai hook |
| `RecoveryContext` | Các **cổng** môi trường: `PoseProvider`, `CollisionChecker`, `PlanProvider`, con trỏ costmap |
| `RecoveryGoal` | Mục tiêu runtime mỗi lượt: `trigger`, `angle`, `distance`, `target_pose`, `params` |
| `RecoveryResult` | Kết quả một tick: status, output theo họ, progress/remaining/elapsed/message |
| `RecoveryRegistry` | Danh sách behavior **có thứ tự**, nạp từ YAML qua Boost.DLL |
| `adapters/` | `CostmapPoseProvider`, `CostmapCollisionChecker` — nối cổng vào costmap thật |
| `plugins/` | Bộ mặc định: wait, clear costmap, rotate, back up |
## Ai giữ bất biến gì
Đây là điểm khác quan trọng nhất so với bản trước, nơi state là `protected` và plugin ghi thẳng
được vào `status_`.
**Base giữ:**
- guard vòng đời: `start` sau `configure`, `update` sau `start`, `configure` chỉ một lần;
- cổng bắt buộc theo `outputKind()` — họ velocity không có `PoseProvider`/`CollisionChecker` thì
`configure()` trả `false` ngay, chứ không để plugin phát hiện lúc đang lái;
- validate goal (NaN/Inf, `distance > 0`) trước khi plugin nhìn thấy;
- đo `dt``elapsed` bằng đồng hồ **thật**, ép `timeout`;
- cưỡng chế `output_type ∈ {outputKind(), kNone}`;
- chặn NaN/Inf trong lệnh vận tốc;
- sinh **stop output đúng họ** ở mọi nhánh guard/cancel/timeout.
**Plugin giữ:** đọc param riêng, chốt mục tiêu lượt này, và sinh một tick an toàn. Toàn bộ state của
base là `private`; plugin truy cập qua accessor `const`.
## Luồng runtime
```
configure(name, ctx) // 1 lần: cache ctx, đọc config chung, onConfigure()
configure(name, ctx, nh) # 1 lần. Kiểm cổng bắt buộc, đọc timeout, gọi onConfigure(nh).
start(goal) // mỗi lượt: chốt mc tiêu runtime, onStart()
start(goal, now) # mỗi lượt. Validate goal, mc thời gian, gọi onStart(goal) -> bool.
├── one-shot (họ A/B): update() 1 lần ───────────► RecoveryResult{status, path|none, msg}
├── kNone : update() -> làm việc / đếm giờ ─────► {status, elapsed, message}
├── kVelocity : loop { update(now) } ───────────────► {status, Twist, progress, remaining}
└── kPath : update() -> sinh path ──────────────► {status, Path}
└── per-cycle (họ C): loop { update() } ────────► RecoveryResult{status, velocity,
(tới khi status != kRunning) progress, remaining}
cancel() # tick kế tiếp gọi onCancel() -> stop output + kCancelled.
```
## Ghi chú thiết kế
## Quyết định thiết kế
- Template-method: base xử lý guard vòng đời (configure→start→update) và `cancel` một chỗ;
plugin không lặp lại các guard này.
- Mục tiêu là RUNTIME qua `RecoveryGoal` (không cố định trong config): cùng plugin phục vụ nhiều
yêu cầu góc/khoảng khác nhau. Field = 0 → dùng default plugin đọc ở `onConfigure()`.
- `onUpdate()` lấy pose robot từ costmap/tf bên trong; không truyền pose qua tham số.
- Sau `cancel()`, base tự trả stop output (Twist 0) + `kCancelled`.
- Param RIÊNG của plugin đọc trong `onConfigure()`.
- Interface KHÔNG include Boost.DLL; export/import là việc của plugin/loader.
- Plugin mẫu có dùng Boost.DLL alias, nhưng core contract vẫn không biết loader/adaptor.
**Cổng, không phải dữ liệu, nằm trong context.** `PlanProvider` là accessor chứ không phải con trỏ
tới `std::vector` vì runtime dùng triple-buffer plan và **xoay con trỏ** giữa ba bộ đệm — một con
trỏ cache lúc `configure()` sẽ trỏ vào bộ đệm scratch sau vài chu kỳ planner. Cùng lý do đó, con trỏ
costmap phải được caller làm mới trước mỗi lượt và plugin không được cache `Costmap2D*` bên trong.
**`std::optional` thay cho sentinel 0.** `goal.angle = 0` là yêu cầu hợp lệ ("đừng quay") và phải
phân biệt được với "caller không đặt". Quy ước cũ "0 nghĩa là dùng default" khiến một góc tính từ
hình học ra ~0 bị âm thầm thay bằng π/2.
**Họ output nằm trong kiểu.** `outputKind()` khai một lần và base cưỡng chế. Trước kia mỗi kết quả
tự mang `output_type` nên nó không dùng để route được: một behavior họ path báo `kVelocity` ở tick
đầu, còn base thì sinh `Velocity(zero)` cho mọi họ ở nhánh terminal.
**Param do caller cấp namespace.** `configure()` nhận `robot::NodeHandle&` đã scope sẵn; plugin
không tự dựng `NodeHandle("~/" + name)` và tự nạp YAML từ disk. Nhờ vậy test chỉ cần trỏ vào cây
config của mình, không phải dựng cây config production.
**Không có `RecoveryDirective`.** Từng cân nhắc thêm kênh "xin lập plan lại", nhưng state machine
của `move_base2` đã chuyển `RECOVERING → PLANNING` với `start_planner = true` trên **cả**
`kSucceeded` lẫn `kFailed` — replan sau recovery đã là hành vi mặc định, thêm kênh riêng là thêm thứ
không ai đọc.
**`kCancelled` không qua ranh giới port.** `move_base2` không tick recovery sau khi cancel (state
chuyển sang `CANCELLING`, nơi mọi nguồn vận tốc bị khoá). Đường `onCancel()` vẫn phải đúng vì nó là
hàng rào cho host khác, nhưng `RecoveryTick` của `move_base2` không cần giá trị tương ứng.
## Điều kiện đóng lại phần legacy
`robot_nav_core::RecoveryBehavior``robot_clear_costmap_recovery` chỉ còn được tham chiếu ở hai
nơi, và cả hai biến mất cùng lúc khi `move_base2` thay xong `move_base` cũ:
| Nơi | Xử lý |
|---|---|
| `move_base/src/move_base.cpp` (loader gen-1) | Bị `move_base2` thay, không sửa |
| `robot_clear_costmap_recovery` | Bị `ClearCostmapRecovery` gen-2 thay, không port |
Khoá `recovery_behaviors:` trong `move_base_common_params.yaml` đã được gỡ: các entry cũ trỏ tên
alias vào `.so` gen-2 trong khi loader ở đó import theo chữ ký gen-1, và Boost.DLL không kiểm kiểu
qua ranh giới `.so`.

View File

@@ -1,110 +1,160 @@
# Hướng Dẫn Viết Plugin recovery_core
# Viết một recovery behavior mới
Package hiện có 4 plugin mẫu dưới `plugins/`:
- `clear_costmap_recovery` — nhóm B, one-shot, no output.
- `rotate_recovery` — nhóm C, per-cycle velocity.
- `back_up_recovery` — nhóm C, per-cycle velocity.
- `regen_path_recovery` — nhóm A, path output.
## 1. Chọn họ output
## Bước chung
Quyết định đầu tiên và không đổi được về sau: `outputKind()`.
1. Kế thừa `recovery_core::RecoveryBehavior`.
2. Override hook `onConfigure()` (tuỳ chọn) — đọc param riêng qua `robot::NodeHandle("~/" + name)`;
ngữ cảnh tf/global_path/costmap lấy qua `ctx()`.
3. Override `onStart(goal)` + `onUpdate()` theo họ (xem dưới). KHÔNG override
`configure/start/update/cancel` — base đã lo guard vòng đời/cancel.
4. Thêm factory `static RecoveryBehaviorPtr create()` **không tham số** + `BOOST_DLL_ALIAS(...)`.
| Họ | Khi nào | Cổng bắt buộc trong context |
|---|---|---|
| `kNone` | Behavior không lái robot (đợi, xoá costmap, gọi thiết bị ngoài) | — |
| `kVelocity` | Behavior tự lái từng cycle | `PoseProvider` + `CollisionChecker` |
| `kPath` | Behavior sinh ra đường đi mới | `PlanProvider` |
## Vòng đời (goal-driven)
Base kiểm cổng theo họ ngay ở `configure()`, nên một behavior họ velocity thiếu collision checker sẽ
**không nạp được**, thay vì phát hiện lúc đang lái.
```
configure(name, ctx) // 1 lần: cache ctx, đọc config chung, gọi onConfigure()
start(goal) // mỗi lượt: chốt mục tiêu RUNTIME (angle/distance/pose), gọi onStart()
loop update() // mỗi cycle tới khi status != kRunning; base guard vòng đời/cancel
[cancel()] // update() kế tiếp -> stop output + kCancelled
```
`RecoveryGoal` là điểm mấu chốt: cùng plugin, mỗi lượt caller đặt `goal.angle` (rad) hay
`goal.distance` (m) khác nhau; field = 0 nghĩa là dùng default đã cấu hình. Override thêm truyền
qua `goal.params` (vd `goal.params["angular_speed"] = 0.8`).
## Override theo họ
| Họ | Override | Trả về |
|----|----------|--------|
| A. path | `onUpdate()` (one-shot) | `RecoveryResult::PathOut(path, kSucceeded)` |
| B. none | `onUpdate()` (one-shot) | `RecoveryResult::Succeeded()` / `Failed()` |
| C. velocity | `onStart()` chốt goal + `onUpdate()` mỗi cycle | `RecoveryResult::Velocity(twist, kRunning\|kSucceeded)` |
Mọi kết quả nên gắn feedback qua `.withProgress(progress, remaining)``.withMessage(...)` để
caller giám sát tiến độ (progress ∈ [0,1], remaining theo rad/m).
## Export bằng Boost.DLL (bắt buộc cho plugin)
## 2. Khung plugin
```cpp
#include <recovery_core/recovery_behavior.h>
#include <boost/dll/alias.hpp>
#include <recovery_core/recovery_math.h>
namespace recovery_plugins {
class SpinRecovery : public recovery_core::RecoveryBehavior {
public:
static recovery_core::RecoveryBehavior::RecoveryBehaviorPtr create() {
return std::make_shared<SpinRecovery>();
#include <boost/dll/alias.hpp>
#include <robot/robot.h>
namespace recovery_plugins
{
namespace
{
constexpr double kDefaultLimit = 1.0; // [m] đơn vị ghi ngay tại chỗ khai báo
}
class MyRecovery final : public recovery_core::RecoveryBehavior
{
public:
MyRecovery() = default;
static recovery_core::RecoveryBehavior::RecoveryBehaviorPtr create()
{
return std::make_shared<MyRecovery>();
}
protected:
// override onConfigure()/onStart(goal)/onUpdate()...
recovery_core::RecoveryOutputType outputKind() const override
{
return recovery_core::RecoveryOutputType::kVelocity;
}
protected:
// nh ĐÃ được caller scope vào namespace param của instance này — đọc khoá phẳng.
bool onConfigure(robot::NodeHandle& nh) override
{
nh.param("limit", limit_, kDefaultLimit);
if (!std::isfinite(limit_) || limit_ <= 0.0)
{
robot::log_warning("[recovery_core] '%s': limit=%.3f không hợp lệ; dùng %.3f.",
name().c_str(), limit_, kDefaultLimit);
limit_ = kDefaultLimit;
}
return true; // false = không chạy được; registry bỏ behavior này và log đích danh
}
// Chốt mục tiêu lượt này + kiểm điều kiện an toàn để khởi động. KHÔNG sinh tick ở đây.
bool onStart(const recovery_core::RecoveryGoal& goal) override
{
if (!ctx().pose->getRobotPose(start_pose_))
return false; // không biết robot ở đâu -> từ chối khởi động
target_ = goal.distance.value_or(limit_);
return true;
}
// dt là thời gian THẬT tính từ tick trước; tick đầu ngay sau start() có dt = 0.
recovery_core::RecoveryResult onUpdate(const robot::Time& now, double dt) override
{
robot_geometry_msgs::PoseStamped pose;
if (!ctx().pose->getRobotPose(pose))
return stopResult(recovery_core::RecoveryStatus::kFailed).withMessage("mất pose robot");
const double done = -recovery_core::projectOntoHeading(pose, start_pose_, start_yaw_);
if (done >= target_)
return stopResult(recovery_core::RecoveryStatus::kSucceeded).withProgress(1.0, 0.0);
robot_geometry_msgs::Twist cmd;
// ... tính cmd, nhớ ramp theo acc_lim và kiểm collision ở pose dự đoán ...
return recovery_core::RecoveryResult::Velocity(cmd, recovery_core::RecoveryStatus::kRunning)
.withProgress(done / target_, target_ - done);
}
// Tuỳ chọn: cơ hội giảm tốc thay vì nhảy thẳng về 0.
recovery_core::RecoveryResult onCancel() override
{
return stopResult(recovery_core::RecoveryStatus::kCancelled).withMessage("cancelled");
}
private:
double limit_ = kDefaultLimit; ///< [m]
double target_ = kDefaultLimit; ///< [m]
double start_yaw_ = 0.0; ///< [rad]
robot_geometry_msgs::PoseStamped start_pose_;
};
} // namespace recovery_plugins
// alias = `type` dùng trong YAML recovery_behaviors.
BOOST_DLL_ALIAS(recovery_plugins::SpinRecovery::create, spin_recovery)
BOOST_DLL_ALIAS(recovery_plugins::MyRecovery::create, MyRecovery)
```
## Nạp phía loader (adapter/caller — không nằm trong recovery_core)
Những gì **không** phải viết: guard `configured_`/`started_`, kiểm cancel, đo `elapsed`, ép
`timeout`, đặt `output_type` cho stop output, kiểm NaN của lệnh vận tốc. Base làm hết — và giữ được
vì toàn bộ state của nó là `private`.
```cpp
#include <boost/dll/import.hpp>
auto loader = boost::dll::import_alias<recovery_core::RecoveryBehavior::RecoveryBehaviorPtr()>(
path_so, /*symbol=*/type, boost::dll::load_mode::append_decorations);
recovery_core::RecoveryBehavior::RecoveryBehaviorPtr behavior = loader();
## 3. Đăng ký build
recovery_core::RecoveryContext ctx;
ctx.tf = tf; ctx.global_path = global_path;
ctx.global_costmap = global_costmap; ctx.local_costmap = local_costmap;
behavior->configure(name, ctx);
recovery_core::RecoveryGoal goal;
goal.angle = 1.57; // "quay 90 độ ngay lượt này"
recovery_core::RecoveryResult r = behavior->start(goal);
while (r.status == recovery_core::RecoveryStatus::kRunning) {
r = behavior->update(); // publish r.command; đọc r.progress/r.remaining/r.message
}
```cmake
add_recovery_core_plugin(
recovery_core_my_recovery
plugins/my_recovery.cpp
)
```
Lưu ý: adapter/test phải giữ handle `.so` sống lâu hơn object plugin. Nếu library bị unload trong
khi object plugin còn tồn tại, virtual call qua vtable của plugin có thể crash.
Thêm tên target vào `catkin_package(LIBRARIES ...)` để consumer khác dùng lại được.
## CMake cho plugin
## 4. Khai trong YAML
- `find_package(Boost REQUIRED COMPONENTS system filesystem)`
- link `${Boost_LIBRARIES}`, `${CMAKE_DL_LIBS}`, `recovery_core`
- `set_target_properties(<plugin> PROPERTIES POSITION_INDEPENDENT_CODE ON)`
- build shared library, tên library + symbol khớp `type`; install `.so` nơi loader tìm.
```yaml
recovery:
behaviors:
- {name: my_instance, type: MyRecovery} # thứ tự trong danh sách CHÍNH LÀ thứ tự thử
my_instance:
limit: 0.5 # [m]
timeout: 10.0 # [s] base đọc; 0 = không giới hạn
## Test plugin
```bash
catkin_make --pkg recovery_core
./devel/lib/recovery_core/recovery_core_plugin_loader_test
MyRecovery:
library_path: librecovery_core_my_recovery # THIẾU KHOÁ NÀY LÀ LỖI PHỔ BIẾN NHẤT
```
Standalone:
Tên alias trong `BOOST_DLL_ALIAS` phải khớp `type`. Namespace param của instance là `<ns>/<name>`
registry dựng `NodeHandle` đó và truyền vào `configure()`; plugin **không** tự đi tìm config trên
disk. Đó là lý do test chỉ cần trỏ vào cây config của mình là chạy được.
```bash
cmake -S src/AMR_T800/pnkx_nav_core/src/Navigations/Libraries/recovery_core -B /tmp/recovery_core_phase3_build
make -C /tmp/recovery_core_phase3_build -j4
/tmp/recovery_core_phase3_build/test/recovery_core_plugin_loader_test
```
Một plugin có thể có **nhiều instance** với tham số khác nhau — bộ mặc định dùng
`ClearCostmapRecovery` hai lần (`conservative_reset``aggressive_reset`).
## 5. Test
Đặt trong `test/`, thêm tên vào danh sách `RECOVERY_CORE_TESTS` của `CMakeLists.txt` (dùng
`catkin_add_gtest`, nên `ctest -R recovery_core` bắt được).
Dùng `recovery_test::VelocityRig` (costmap giả + pose giả + collision checker giả + đồng hồ giả) và
nạp plugin **qua `RecoveryRegistry`** để đi đúng đường Boost.DLL mà runtime dùng — không link thẳng
`.so` vào test.
Tối thiểu phải phủ:
- từ chối khởi động khi điều kiện an toàn không thoả;
- mất pose giữa chừng → `kFailed` + lệnh dừng;
- loop chạy chậm (dt gấp 520 lần nhịp thường) vẫn dừng đúng chỗ;
- cancel → lệnh dừng;
- param ngoài dải → dùng default, không nhận giá trị sai.
Cuối cùng, kiểm rằng test **fail được**: gỡ `.so` khỏi `devel/lib` rồi chạy lại. Phải đỏ. Bộ test cũ
của gói này in `[PASS]` khi không nạp được plugin nào — đó là thứ phải tránh.

View File

@@ -1,30 +1,64 @@
# An Toàn — recovery_core
# An toàn — recovery_core
> Cảnh báo an toàn khi dùng interface này. Đọc trước khi triển khai plugin họ vận tốc.
> Đọc trước khi viết plugin họ vận tốc. Recovery chạy đúng lúc robot đã ở tình huống xấu, nên
> default ở đây phải fail-safe chứ không fail-open.
## recovery_core KHÔNG đảm bảo
## Base bảo đảm
- **Không collision-check tự đng.** Interface không kiểm tra va chạm khi sinh vận tốc
(backup/spin) hay khi tạo path. Việc tránh va chạm là trách nhiệm của **plugin** (dùng
costmap được cấp qua `initialize`) hoặc của **caller**.
- **Không quản lý vòng lặp thời gian thực.** Caller chịu trách nhiệm gọi `update()` đúng nhịp
và publish command.
- **Không đảm bảo frame/đơn vị.** Pose lấy từ costmap/tf phải đúng frame; đơn vị phải nhất quán
(m, rad, s, m/s, rad/s).
- **Guard vòng đời ở base.** `start()` trước `configure()`, hay `update()` trước `start()`, đều trả
`RecoveryResult::Failed()`. Sau `cancel()`, base trả stop output (Twist 0) + `kCancelled`.
Những điều dưới đây đúng kể cả khi plugin viết ẩu — base cưỡng chế, không phụ thuộc plugin nhớ.
## Nguyên tắc cho plugin
| Bất biến | Cơ chế |
|---|---|
| Họ velocity không chạy được nếu thiếu pose hoặc collision checker | `configure()` trả `false` khi `ctx.pose`/`ctx.collision` null |
| `dt` là thời gian **thật**, không phải chu kỳ cấu hình | `update(now)` tính `now - lần trước`; đồng hồ đi lùi → `dt = 0` |
| `elapsed` luôn có mặt trên mọi kết quả | Base ghi, plugin không phải đặt |
| Recovery không treo vô hạn | Param `timeout` [s]; quá hạn → `kFailed` + stop output |
| `output_type` luôn thuộc `{outputKind(), kNone}` | Sai họ → hạ về `kNone`, xoá dữ liệu, log lỗi |
| NaN/Inf không ra được `cmd_vel` | Lệnh không hữu hạn → Twist 0 + `kFailed` |
| Guard vòng đời không cần plugin tự làm | `start` trước `configure`, `update` trước `start``kFailed` + stop output |
| Stop output đúng họ | Họ velocity nhận **Twist 0 tường minh**; họ khác nhận `kNone` |
- Guard costmap/tf null (lấy qua `ctx()`) trước khi thao tác; fail an toàn -> `RecoveryResult::Failed()`.
- Không cần tự guard vòng đời/cancel — base đã lo; plugin tập trung logic recovery.
- Với họ vận tốc: khi không chắc an toàn, trả **stop command** (Twist 0), không trả vận tốc mù.
- Kiểm tra NaN/Inf của pose/vận tốc trước khi xuất command.
- Tôn trọng giới hạn vận tốc/gia tốc của robot (đọc qua param).
- `BackUpRecovery` có param `require_costmap` để bắt buộc có local costmap trước khi xuất
vận tốc lùi. Adapter production vẫn nên có safety gate/collision check riêng trước publish.
## Base KHÔNG bảo đảm
## Trách nhiệm caller/adapter
- **Không thay caller quyết định có nên recovery hay không.** Ngữ cảnh cho phép recovery (đã dừng
planner, không đang ở chế độ thủ công, không đang estop) là việc của caller.
- **Không quản lý nhịp gọi.** Caller phải tick đúng chu kỳ control và publish lệnh.
- **Không là hàng rào vận tốc cuối cùng.** Plugin clamp theo giới hạn của mình; `VelocityArbiter`
phía ngoài vẫn phải clamp lần cuối. Hai tầng, không chồng nhau: plugin lập **kế hoạch** dừng/ramp,
arbiter **chặn** giá trị vượt ngưỡng.
- Đảm bảo ngữ cảnh cho phép recovery (vd: đã dừng planner, vùng xung quanh đủ an toàn).
- Áp timeout/giám sát ngoài để tránh recovery chạy vô hạn.
## Quy tắc cho plugin họ vận tốc
1. **Không tích phân vận tốc lệnh để đo tiến độ.** Dùng `PoseProvider` và hình chiếu delta pose
(`recovery_core::projectOntoHeading`). Tích phân lệnh thì bánh trượt hay robot bị chặn vẫn báo đi
đủ quãng — và đó là lúc nguy hiểm nhất để nói dối.
2. **Kiểm va chạm trên pose *dự đoán*, trước khi phát lệnh.** Không phải sau. `BackUpRecovery`
pose ở cuối chu kỳ tới; `RotateRecovery` quét toàn bộ cung ngay tại `onStart()` và refuse khởi
động nếu có bất kỳ góc nào bị chặn.
3. **Mất pose là dừng.** `getRobotPose()` trả `false` → trả `kFailed` + Twist 0. Không dùng pose cũ.
4. **Ramp theo `acc_lim_*`.** Tick đầu không được nhảy thẳng lên tốc độ tối đa; `onCancel()` cũng
nên giảm tốc thay vì nhảy bậc.
5. **Clamp mọi param có dải.** `angle` ∈ [-2π, 2π], `distance` ∈ (0, `*_max`], tốc độ ≤ trần. Param
sai thì log cảnh báo **kèm giá trị** rồi dùng default — không im lặng.
6. **Ghi rõ dấu.** `linear.x < 0` là lùi; `angular.z > 0` là ngược chiều kim đồng hồ. Param cấu hình
**độ lớn**, dấu do plugin đặt theo ngữ cảnh.
## Quy tắc chung cho mọi plugin
- `onUpdate()` chạy trên thread phát `cmd_vel`. **Không block quá một phần nhỏ chu kỳ control**:
không parse file, không cấp phát lớn, không gọi việc nặng của costmap (`ClearCostmapRecovery` cố
ý không gọi `updateMap()` vì lý do này).
- **Không cache `Costmap2D*`** qua các tick. Con trỏ có thể bị thay giữa hai cycle; lấy lại mỗi lần
dùng.
- Kiểm mã trả về của `CollisionChecker` bằng `< 0`, **không** so với một giá trị âm cụ thể — các
hiện thực trong workspace không thống nhất mã lỗi.
- `configure()` trả `false` khi cấu hình không chạy được, thay vì log warning rồi chạy tiếp bằng
default. Registry sẽ bỏ behavior đó và log đích danh.
## Trách nhiệm của caller
- Làm mới con trỏ costmap trong `RecoveryContext` trước mỗi `start()`/`update()`.
- Tick recovery từ **đúng một thread** — thread sở hữu `cmd_vel`. Thread riêng cho recovery nghĩa là
hai bộ điều khiển cùng phát vận tốc.
- Chỉ gọi `update()` sau khi `start()` trả `true`.
- Sau `cancel()`, hoặc tick tiếp để nhận stop output, hoặc tự khoá nguồn vận tốc về 0.