first commit
This commit is contained in:
239
src/bridges/mission_adapter_bridge.cpp
Normal file
239
src/bridges/mission_adapter_bridge.cpp
Normal file
@@ -0,0 +1,239 @@
|
||||
/*********************************************************************
|
||||
*
|
||||
* Software License Agreement (BSD License)
|
||||
*
|
||||
* move_base2 — cài đặt MissionAdapterBridge.
|
||||
*
|
||||
* Author: DuongTD
|
||||
*********************************************************************/
|
||||
#include <move_base2/bridges/mission_adapter_bridge.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <mission_adapters/mission_manager.h>
|
||||
#include <robot/robot.h>
|
||||
|
||||
namespace move_base2
|
||||
{
|
||||
|
||||
MissionAdapterBridge::MissionAdapterBridge() = default;
|
||||
MissionAdapterBridge::~MissionAdapterBridge() = default;
|
||||
|
||||
void MissionAdapterBridge::attach(mission_adapters::MissionManager* manager)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
manager_ = manager;
|
||||
}
|
||||
|
||||
void MissionAdapterBridge::setCancelCallback(CancelCallback callback)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
cancel_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
void MissionAdapterBridge::setRequestCallback(RequestCallback callback)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
request_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// Chuyển đổi
|
||||
// ================================================================================================
|
||||
|
||||
NavigationRequest MissionAdapterBridge::toRequest(const mission_adapters::Mission& mission)
|
||||
{
|
||||
NavigationRequest request;
|
||||
|
||||
request.mission_sequence_id = mission.id;
|
||||
request.has_goal = mission.has_goal;
|
||||
request.goal = mission.goal;
|
||||
|
||||
// Sai số để mặc định: quy ước của NavigationRequest là giá trị <= 0 nghĩa "dùng default của
|
||||
// profile trong config". Mission layer không biết gì về sai số hình học nên không được đặt.
|
||||
request.tolerance = GoalTolerance();
|
||||
|
||||
// Mission layer KHÔNG diễn giải actionType và không lọc gì (D8) — action đi qua nguyên vẹn, đúng
|
||||
// thứ tự đã sắp theo sequenceId.
|
||||
request.actions.reserve(mission.actions.size());
|
||||
for (const auto& action : mission.actions)
|
||||
{
|
||||
request.actions.push_back(action.action);
|
||||
}
|
||||
|
||||
// Mọi mission đều chạy profile position. `MissionType` chỉ nói mission đến TỪ ĐÂU (goal đơn hay
|
||||
// order VDA5050), không nói robot phải di chuyển KIỂU gì — docking/go-straight/rotate là lựa chọn
|
||||
// của người vận hành qua sáu entry point của contract host, không phải của mission layer.
|
||||
request.profile = MotionProfile::kPosition;
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// NavigationClient — gọi từ thread của MissionExecutor
|
||||
// ================================================================================================
|
||||
|
||||
bool MissionAdapterBridge::dispatch(const std::shared_ptr<const mission_adapters::Mission>& mission)
|
||||
{
|
||||
if (!mission)
|
||||
{
|
||||
robot::log_error("[move_base2] MissionAdapterBridge: dispatch(nullptr).\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
if (!running_)
|
||||
{
|
||||
// Chưa start hoặc đã stop. Từ chối thay vì cất lại: mission layer phải biết chặng của nó không
|
||||
// được nhận, chứ không phải chờ một kết quả sẽ không bao giờ tới.
|
||||
robot::log_warning("[move_base2] MissionAdapterBridge: từ chối mission %llu — bridge chưa "
|
||||
"start.\n", static_cast<unsigned long long>(mission->id));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pending_)
|
||||
{
|
||||
// Không nên xảy ra: MissionManager chỉ giao chặng mới sau khi chặng cũ kết thúc. Đếm lại thay
|
||||
// vì im lặng — một chặng biến mất trong khi fleet master vẫn chờ nó là lỗi rất khó truy.
|
||||
++dropped_requests_;
|
||||
robot::log_warning("[move_base2] MissionAdapterBridge: mission %llu đè mission %llu chưa kịp "
|
||||
"đẩy xuống.\n", static_cast<unsigned long long>(mission->id),
|
||||
static_cast<unsigned long long>(pending_->id));
|
||||
}
|
||||
|
||||
// Chỉ cất lại. Chuyển đổi và đẩy xuống navigation xảy ra ở pumpPendingRequest(), trên control
|
||||
// thread — xem doc của lớp.
|
||||
pending_ = mission;
|
||||
return true;
|
||||
}
|
||||
|
||||
void MissionAdapterBridge::cancelActive(mission_adapters::MissionId /*id*/)
|
||||
{
|
||||
CancelCallback callback;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
// Chặng đang chờ mà chưa kịp xuống navigation thì huỷ ngay tại đây: đẩy nó xuống rồi mới huỷ là
|
||||
// cho robot nhúc nhích một cycle vì một chặng đã bị thu hồi.
|
||||
pending_.reset();
|
||||
callback = cancel_callback_;
|
||||
}
|
||||
|
||||
// Gọi NGOÀI lock: callback đi vào control loop, không được chạy dưới mutex của bridge.
|
||||
if (callback)
|
||||
{
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// Biên thread — chỉ control thread gọi
|
||||
// ================================================================================================
|
||||
|
||||
bool MissionAdapterBridge::pumpPendingRequest()
|
||||
{
|
||||
std::shared_ptr<const mission_adapters::Mission> mission;
|
||||
RequestCallback callback;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!pending_ || !request_callback_)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
mission = pending_;
|
||||
pending_.reset();
|
||||
callback = request_callback_;
|
||||
}
|
||||
|
||||
// Ngoài lock: callback đi thẳng vào ControlLoop::submit.
|
||||
callback(toRequest(*mission));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// MissionPort — gọi từ control thread
|
||||
// ================================================================================================
|
||||
|
||||
void MissionAdapterBridge::reportOutcome(std::uint64_t mission_sequence_id,
|
||||
NavigationOutcome outcome)
|
||||
{
|
||||
if (mission_sequence_id == mission_adapters::kInvalidMissionId)
|
||||
{
|
||||
// Goal trực tiếp từ contract host, không thuộc mission nào. Không có gì để báo.
|
||||
return;
|
||||
}
|
||||
|
||||
mission_adapters::MissionManager* manager = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
manager = manager_;
|
||||
}
|
||||
|
||||
if (manager == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Chỉ kSucceeded mới là "chặng xong". Ba kết cục còn lại đều là "chặng không hoàn thành", và
|
||||
// chính sách hàng đợi (`clear_queue_on_failure`) nằm ở mission layer chứ không ở đây.
|
||||
//
|
||||
// kPreempted đáng chú ý: mission layer thường đã chuyển sang chặng khác rồi, nên
|
||||
// onNavigationFailed sẽ trả false và không làm gì — đúng ý, chứ không phải bị bỏ sót.
|
||||
const bool accepted = (outcome == NavigationOutcome::kSucceeded)
|
||||
? manager->onNavigationDone(mission_sequence_id)
|
||||
: manager->onNavigationFailed(mission_sequence_id);
|
||||
|
||||
if (!accepted)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
++stale_outcomes_;
|
||||
}
|
||||
}
|
||||
|
||||
bool MissionAdapterBridge::hasActiveMission() const
|
||||
{
|
||||
mission_adapters::MissionManager* manager = nullptr;
|
||||
bool has_pending = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
manager = manager_;
|
||||
has_pending = static_cast<bool>(pending_);
|
||||
}
|
||||
|
||||
if (has_pending)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return manager != nullptr && manager->hasMission();
|
||||
}
|
||||
|
||||
void MissionAdapterBridge::start()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
running_ = true;
|
||||
}
|
||||
|
||||
void MissionAdapterBridge::stop()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
running_ = false;
|
||||
// Bỏ chặng đang chờ: nó sẽ không bao giờ được chạy, và giữ lại chỉ để nó chạy sau một lần start()
|
||||
// sau này là hành vi không ai mong đợi.
|
||||
pending_.reset();
|
||||
}
|
||||
|
||||
std::size_t MissionAdapterBridge::droppedRequests() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return dropped_requests_;
|
||||
}
|
||||
|
||||
std::size_t MissionAdapterBridge::staleOutcomes() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return stale_outcomes_;
|
||||
}
|
||||
|
||||
} // namespace move_base2
|
||||
261
src/config/move_base2_config.cpp
Normal file
261
src/config/move_base2_config.cpp
Normal file
@@ -0,0 +1,261 @@
|
||||
/*********************************************************************
|
||||
* move_base2 — đọc và validate cấu hình runtime.
|
||||
*
|
||||
* Author: DuongTD
|
||||
*********************************************************************/
|
||||
#include <move_base2/config/move_base2_config.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
|
||||
#include <robot/robot.h>
|
||||
|
||||
namespace move_base2
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
/// Đọc một khoá double; thiếu khoá thì giữ nguyên default và nói rõ khoá nào bị thiếu.
|
||||
void readDouble(robot::NodeHandle& nh, const std::string& key, double& value)
|
||||
{
|
||||
if (!nh.hasParam(key))
|
||||
{
|
||||
robot::log_warning("[move_base2] thiếu param '%s', dùng default %.4f", key.c_str(), value);
|
||||
return;
|
||||
}
|
||||
nh.param(key, value, value);
|
||||
}
|
||||
|
||||
void readInt(robot::NodeHandle& nh, const std::string& key, int& value)
|
||||
{
|
||||
if (!nh.hasParam(key))
|
||||
{
|
||||
robot::log_warning("[move_base2] thiếu param '%s', dùng default %d", key.c_str(), value);
|
||||
return;
|
||||
}
|
||||
nh.param(key, value, value);
|
||||
}
|
||||
|
||||
void readBool(robot::NodeHandle& nh, const std::string& key, bool& value)
|
||||
{
|
||||
if (!nh.hasParam(key))
|
||||
{
|
||||
robot::log_warning("[move_base2] thiếu param '%s', dùng default %s", key.c_str(),
|
||||
value ? "true" : "false");
|
||||
return;
|
||||
}
|
||||
nh.param(key, value, value);
|
||||
}
|
||||
|
||||
void readString(robot::NodeHandle& nh, const std::string& key, std::string& value)
|
||||
{
|
||||
if (!nh.hasParam(key))
|
||||
{
|
||||
robot::log_warning("[move_base2] thiếu param '%s', dùng default '%s'", key.c_str(),
|
||||
value.c_str());
|
||||
return;
|
||||
}
|
||||
nh.param(key, value, value);
|
||||
}
|
||||
|
||||
/// Đọc một binding profile từ namespace con cùng tên.
|
||||
void readBinding(robot::NodeHandle& nh, const std::string& ns, ProfileBinding& binding)
|
||||
{
|
||||
robot::NodeHandle profile_nh(nh, ns);
|
||||
readString(profile_nh, "base_global_planner", binding.global_planner_name);
|
||||
readString(profile_nh, "base_local_planner", binding.local_planner_name);
|
||||
readDouble(profile_nh, "xy_goal_tolerance", binding.default_xy_tolerance);
|
||||
readDouble(profile_nh, "yaw_goal_tolerance", binding.default_yaw_tolerance);
|
||||
}
|
||||
|
||||
bool validateBinding(const ProfileBinding& binding, const char* name, std::string& error)
|
||||
{
|
||||
if (binding.local_planner_name.empty())
|
||||
{
|
||||
// Không đặt là hợp lệ: deployment có thể không dùng profile đó. Nhưng nếu đã đặt planner thì
|
||||
// sai số phải hợp lệ, vì chúng đi thẳng vào điều kiện dừng.
|
||||
return true;
|
||||
}
|
||||
if (!std::isfinite(binding.default_xy_tolerance) || binding.default_xy_tolerance <= 0.0)
|
||||
{
|
||||
error = std::string(name) + ".xy_goal_tolerance phải > 0 [m]";
|
||||
return false;
|
||||
}
|
||||
if (!std::isfinite(binding.default_yaw_tolerance) || binding.default_yaw_tolerance <= 0.0)
|
||||
{
|
||||
error = std::string(name) + ".yaw_goal_tolerance phải > 0 [rad]";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Đọc cấu hình đường vào cảm biến từ namespace con `sensors`.
|
||||
void readSensors(robot::NodeHandle& nh, SensorGatewayConfig& sensors)
|
||||
{
|
||||
robot::NodeHandle sensors_nh(nh, "sensors");
|
||||
readBool(sensors_nh, "laser_sor_enabled", sensors.laser_sor_enabled);
|
||||
readInt(sensors_nh, "laser_sor_mean_k", sensors.laser_sor_mean_k);
|
||||
readDouble(sensors_nh, "laser_sor_stddev_mul", sensors.laser_sor_stddev_mul);
|
||||
}
|
||||
|
||||
void describeBinding(std::ostringstream& out, const char* name, const ProfileBinding& binding)
|
||||
{
|
||||
out << " " << name << ": global='" << binding.global_planner_name << "' local='"
|
||||
<< binding.local_planner_name << "' xy=" << binding.default_xy_tolerance
|
||||
<< " m yaw=" << binding.default_yaw_tolerance << " rad\n";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void MoveBase2Config::fromNodeHandle(robot::NodeHandle& nh)
|
||||
{
|
||||
readDouble(nh, "controller_frequency", controller_frequency);
|
||||
readDouble(nh, "planner_frequency", planner_frequency);
|
||||
readDouble(nh, "planner_timeout", planner_timeout);
|
||||
|
||||
readDouble(nh, "planner_patience", state_machine.planner_patience);
|
||||
readDouble(nh, "controller_patience", state_machine.controller_patience);
|
||||
readDouble(nh, "oscillation_timeout", state_machine.oscillation_timeout);
|
||||
readDouble(nh, "oscillation_distance", state_machine.oscillation_distance);
|
||||
readDouble(nh, "action_patience", state_machine.action_patience);
|
||||
readInt(nh, "max_planning_retries", state_machine.max_planning_retries);
|
||||
readBool(nh, "recovery_behavior_enabled", state_machine.recovery_enabled);
|
||||
|
||||
readDouble(nh, "max_vel_x", velocity.max_vel_x);
|
||||
readDouble(nh, "min_vel_x", velocity.min_vel_x);
|
||||
readDouble(nh, "max_vel_theta", velocity.max_vel_theta);
|
||||
readDouble(nh, "acc_lim_x", velocity.max_accel_x);
|
||||
readDouble(nh, "acc_lim_theta", velocity.max_accel_theta);
|
||||
|
||||
readSensors(nh, sensors);
|
||||
|
||||
readBinding(nh, "position", position);
|
||||
readBinding(nh, "docking", docking);
|
||||
readBinding(nh, "go_straight", go_straight);
|
||||
readBinding(nh, "rotate", rotate);
|
||||
|
||||
readString(nh, "recovery_namespace", recovery_namespace);
|
||||
readString(nh, "action_namespace", action_namespace);
|
||||
readString(nh, "mission_namespace", mission_namespace);
|
||||
readString(nh, "global_frame", global_frame);
|
||||
readString(nh, "robot_base_frame", robot_base_frame);
|
||||
|
||||
// recovery_behavior_count KHÔNG đọc từ YAML: nó là số behavior thực sự nạp được, do
|
||||
// RecoveryRunner báo lại sau khi configure. Đọc từ config thì một behavior hỏng sẽ khiến state
|
||||
// machine tin là vẫn còn đường phục hồi.
|
||||
}
|
||||
|
||||
bool MoveBase2Config::validate(std::string& error) const
|
||||
{
|
||||
if (!std::isfinite(controller_frequency) || controller_frequency <= 0.0)
|
||||
{
|
||||
error = "controller_frequency phải > 0 [Hz]";
|
||||
return false;
|
||||
}
|
||||
if (controller_frequency > 200.0)
|
||||
{
|
||||
error = "controller_frequency > 200 Hz — nhịp này không thực tế cho một control loop có costmap";
|
||||
return false;
|
||||
}
|
||||
if (!std::isfinite(planner_frequency) || planner_frequency < 0.0)
|
||||
{
|
||||
error = "planner_frequency phải >= 0 [Hz] (0 = chỉ lập plan khi cần)";
|
||||
return false;
|
||||
}
|
||||
if (!std::isfinite(planner_timeout))
|
||||
{
|
||||
error = "planner_timeout không hữu hạn [s]";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (recovery_namespace.empty())
|
||||
{
|
||||
error = "recovery_namespace rỗng";
|
||||
return false;
|
||||
}
|
||||
if (global_frame.empty() || robot_base_frame.empty())
|
||||
{
|
||||
error = "global_frame và robot_base_frame không được rỗng";
|
||||
return false;
|
||||
}
|
||||
if (global_frame == robot_base_frame)
|
||||
{
|
||||
error = "global_frame trùng robot_base_frame — pose robot sẽ luôn là gốc toạ độ";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!validateBinding(position, "position", error) ||
|
||||
!validateBinding(docking, "docking", error) ||
|
||||
!validateBinding(go_straight, "go_straight", error) ||
|
||||
!validateBinding(rotate, "rotate", error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (position.local_planner_name.empty() && docking.local_planner_name.empty() &&
|
||||
go_straight.local_planner_name.empty() && rotate.local_planner_name.empty())
|
||||
{
|
||||
error = "không profile nào có base_local_planner — runtime sẽ từ chối mọi yêu cầu";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!velocity.validate(error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sensors.validate(error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sau cùng: struct con. Thứ tự này có chủ đích — báo lỗi ở tầng cụ thể nhất trước, để thông báo
|
||||
// nói đúng khoá YAML mà người vận hành cần sửa, chứ không phải một ràng buộc phái sinh.
|
||||
//
|
||||
// Lưu ý ràng buộc thứ tự KHỞI TẠO: `state_machine.recovery_behavior_count` KHÔNG đến từ YAML mà
|
||||
// là số behavior RecoveryRunner nạp được thật. Bên gọi phải điền nó trước khi gọi hàm này —
|
||||
// xem @ref MoveBase2Config::validate trong header.
|
||||
if (!state_machine.validate(error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string MoveBase2Config::describe() const
|
||||
{
|
||||
std::ostringstream out;
|
||||
out << "move_base2 config:\n";
|
||||
out << " controller_frequency: " << controller_frequency << " Hz\n";
|
||||
out << " planner_frequency: " << planner_frequency << " Hz\n";
|
||||
out << " planner_timeout: " << planner_timeout << " s\n";
|
||||
out << " frames: global='" << global_frame << "' base='" << robot_base_frame << "'\n";
|
||||
out << " namespaces: recovery='" << recovery_namespace << "' actions='" << action_namespace
|
||||
<< "' mission='" << mission_namespace << "'\n";
|
||||
describeBinding(out, "position", position);
|
||||
describeBinding(out, "docking", docking);
|
||||
describeBinding(out, "go_straight", go_straight);
|
||||
describeBinding(out, "rotate", rotate);
|
||||
out << state_machine.describe();
|
||||
out << velocity.describe();
|
||||
out << sensors.describe();
|
||||
return out.str();
|
||||
}
|
||||
|
||||
ControlLoopConfig MoveBase2Config::toControlLoopConfig() const
|
||||
{
|
||||
ControlLoopConfig config;
|
||||
config.state_machine = state_machine;
|
||||
config.velocity = velocity;
|
||||
config.nominal_control_period =
|
||||
controller_frequency > 0.0 ? 1.0 / controller_frequency : 0.05; // [s]
|
||||
config.robot_base_frame = robot_base_frame;
|
||||
config.position = position;
|
||||
config.docking = docking;
|
||||
config.go_straight = go_straight;
|
||||
config.rotate = rotate;
|
||||
return config;
|
||||
}
|
||||
|
||||
} // namespace move_base2
|
||||
569
src/control_loop.cpp
Normal file
569
src/control_loop.cpp
Normal file
@@ -0,0 +1,569 @@
|
||||
/*********************************************************************
|
||||
*
|
||||
* Software License Agreement (BSD License)
|
||||
*
|
||||
* move_base2 — cài đặt control loop.
|
||||
*
|
||||
* Author: DuongTD
|
||||
*********************************************************************/
|
||||
#include <move_base2/control_loop.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
|
||||
namespace move_base2
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
/// [-] Sai lệch chuẩn quaternion còn chấp nhận được trước khi coi goal là hỏng.
|
||||
constexpr double kQuaternionNormTolerance = 1e-2;
|
||||
|
||||
} // namespace
|
||||
|
||||
// ================================================================================================
|
||||
// ControlLoopConfig
|
||||
// ================================================================================================
|
||||
|
||||
bool ControlLoopConfig::validate(std::string& error) const
|
||||
{
|
||||
if (!state_machine.validate(error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!velocity.validate(error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!(nominal_control_period > 0.0))
|
||||
{
|
||||
error = "nominal_control_period phải > 0 [s]";
|
||||
return false;
|
||||
}
|
||||
if (position.local_planner_name.empty())
|
||||
{
|
||||
error = "profile 'position' bắt buộc phải có local_planner_name";
|
||||
return false;
|
||||
}
|
||||
if (robot_base_frame.empty())
|
||||
{
|
||||
// Frame rỗng đi thẳng vào header của lệnh vận tốc gửi host. Chặn ở đây thay vì để host nhận một
|
||||
// lệnh không biết thuộc hệ toạ độ nào.
|
||||
error = "robot_base_frame không được rỗng";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string ControlLoopConfig::describe() const
|
||||
{
|
||||
std::ostringstream out;
|
||||
out << state_machine.describe();
|
||||
out << velocity.describe();
|
||||
out << "ControlLoop:\n";
|
||||
out << " nominal_control_period: " << nominal_control_period << " s\n";
|
||||
out << " robot_base_frame : " << robot_base_frame << '\n';
|
||||
out << " profile position : " << position.global_planner_name << " / "
|
||||
<< position.local_planner_name << '\n';
|
||||
out << " profile docking : " << docking.global_planner_name << " / "
|
||||
<< docking.local_planner_name << '\n';
|
||||
out << " profile go_straight: " << go_straight.global_planner_name << " / "
|
||||
<< go_straight.local_planner_name << '\n';
|
||||
out << " profile rotate : " << rotate.global_planner_name << " / "
|
||||
<< rotate.local_planner_name << '\n';
|
||||
return out.str();
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// ControlLoop
|
||||
// ================================================================================================
|
||||
|
||||
bool ControlLoop::configure(const ControlLoopConfig& config, const ControlLoopDeps& deps,
|
||||
std::string& error)
|
||||
{
|
||||
initialized_ = false;
|
||||
|
||||
if (deps.clock == nullptr || deps.pose == nullptr || deps.planner == nullptr ||
|
||||
deps.controller == nullptr || deps.recovery == nullptr)
|
||||
{
|
||||
error = "thiếu cổng bắt buộc (clock/pose/planner/controller/recovery)";
|
||||
return false;
|
||||
}
|
||||
if (!config.validate(error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!state_machine_.configure(config.state_machine, error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!arbiter_.configure(config.velocity, error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
config_ = config;
|
||||
deps_ = deps;
|
||||
initialized_ = true;
|
||||
reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ControlLoop::reset()
|
||||
{
|
||||
state_machine_.reset();
|
||||
arbiter_.reset();
|
||||
|
||||
has_pending_request_ = false;
|
||||
has_active_request_ = false;
|
||||
pause_requested_ = false;
|
||||
resume_requested_ = false;
|
||||
cancel_requested_ = false;
|
||||
|
||||
planner_feedback_ = PlannerFeedback::kIdle;
|
||||
controller_feedback_ = ControllerFeedback::kIdle;
|
||||
recovery_feedback_ = RecoveryFeedback::kIdle;
|
||||
action_feedback_ = ActionFeedback::kIdle;
|
||||
|
||||
latest_plan_.clear();
|
||||
planner_running_ = false;
|
||||
|
||||
// Nhãn mới + huỷ: lượt đang bay thuộc về vòng đời trước, kết quả của nó không được nhận nhầm.
|
||||
++plan_tag_;
|
||||
if (deps_.planner != nullptr)
|
||||
{
|
||||
deps_.planner->cancelPlan();
|
||||
}
|
||||
|
||||
has_last_cycle_time_ = false;
|
||||
has_oscillation_origin_ = false;
|
||||
|
||||
has_outcome_ = false;
|
||||
outcome_report_count_ = 0;
|
||||
last_reason_ = "";
|
||||
}
|
||||
|
||||
const ProfileBinding* ControlLoop::bindingFor(MotionProfile profile) const
|
||||
{
|
||||
switch (profile)
|
||||
{
|
||||
case MotionProfile::kPosition:
|
||||
return &config_.position;
|
||||
case MotionProfile::kDocking:
|
||||
return &config_.docking;
|
||||
case MotionProfile::kGoStraight:
|
||||
return &config_.go_straight;
|
||||
case MotionProfile::kRotate:
|
||||
return &config_.rotate;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ControlLoop::isQuaternionValid(const robot_geometry_msgs::PoseStamped& pose)
|
||||
{
|
||||
const auto& q = pose.pose.orientation;
|
||||
if (!std::isfinite(q.x) || !std::isfinite(q.y) || !std::isfinite(q.z) || !std::isfinite(q.w))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const double norm_sq = q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w;
|
||||
return std::abs(std::sqrt(norm_sq) - 1.0) <= kQuaternionNormTolerance;
|
||||
}
|
||||
|
||||
bool ControlLoop::submit(const NavigationRequest& request, std::string& reason)
|
||||
{
|
||||
if (!initialized_)
|
||||
{
|
||||
reason = "runtime chưa khởi tạo";
|
||||
return false;
|
||||
}
|
||||
|
||||
// D8: yêu cầu mang action cần có ActionPort; từ chối tại cửa thay vì kẹt sau khi tới goal.
|
||||
if (!request.actions.empty() && deps_.action == nullptr)
|
||||
{
|
||||
reason = "yêu cầu có action nhưng runtime không có action port";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!request.has_goal)
|
||||
{
|
||||
// D8: yêu cầu chỉ-có-action — không có goal để validate, không có planner để swap.
|
||||
if (request.actions.empty())
|
||||
{
|
||||
reason = "yêu cầu không có goal lẫn action";
|
||||
return false;
|
||||
}
|
||||
pending_request_ = request;
|
||||
has_pending_request_ = true;
|
||||
cancel_requested_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!std::isfinite(request.goal.pose.position.x) || !std::isfinite(request.goal.pose.position.y))
|
||||
{
|
||||
reason = "goal có toạ độ không hữu hạn";
|
||||
return false;
|
||||
}
|
||||
if (!isQuaternionValid(request.goal))
|
||||
{
|
||||
reason = "goal có quaternion không hợp lệ";
|
||||
return false;
|
||||
}
|
||||
|
||||
const ProfileBinding* binding = bindingFor(request.profile);
|
||||
if (binding == nullptr || binding->local_planner_name.empty())
|
||||
{
|
||||
reason = std::string("chưa cấu hình planner cho profile '") + toString(request.profile) + "'";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Đổi planner NGAY tại cửa vào, trước khi nhận yêu cầu: nếu không nạp được thì từ chối luôn, chứ
|
||||
// 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.
|
||||
if (!binding->global_planner_name.empty() &&
|
||||
!deps_.planner->swapPlanner(binding->global_planner_name))
|
||||
{
|
||||
reason = "không nạp được global planner '" + binding->global_planner_name + "'";
|
||||
return false;
|
||||
}
|
||||
if (!deps_.controller->swapPlanner(binding->local_planner_name))
|
||||
{
|
||||
reason = "không nạp được local planner '" + binding->local_planner_name + "'";
|
||||
return false;
|
||||
}
|
||||
|
||||
deps_.controller->setTolerance(
|
||||
request.tolerance.hasXy() ? request.tolerance.xy : binding->default_xy_tolerance,
|
||||
request.tolerance.hasYaw() ? request.tolerance.yaw : binding->default_yaw_tolerance);
|
||||
|
||||
pending_request_ = request;
|
||||
has_pending_request_ = true;
|
||||
|
||||
// Yêu cầu mới thay thế yêu cầu đang chờ, không xếp hàng: xếp hàng là việc của mission layer.
|
||||
cancel_requested_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ControlLoop::requestPause()
|
||||
{
|
||||
pause_requested_ = true;
|
||||
resume_requested_ = false;
|
||||
}
|
||||
|
||||
void ControlLoop::requestResume()
|
||||
{
|
||||
resume_requested_ = true;
|
||||
pause_requested_ = false;
|
||||
}
|
||||
|
||||
void ControlLoop::requestCancel()
|
||||
{
|
||||
cancel_requested_ = true;
|
||||
}
|
||||
|
||||
void ControlLoop::collectPlannerResult()
|
||||
{
|
||||
PlanResult result;
|
||||
if (!deps_.planner->pollPlan(result))
|
||||
{
|
||||
// Chưa có kết quả. Chỉ báo "đang lập" khi thật sự có lượt đang chạy VÀ chưa có phản hồi nào
|
||||
// khác — `apply_plan` ở cycle trước có thể đã đặt kFailed khi controller từ chối plan, và đó
|
||||
// là tin quan trọng hơn.
|
||||
if (planner_running_ && planner_feedback_ == PlannerFeedback::kIdle)
|
||||
{
|
||||
planner_feedback_ = PlannerFeedback::kBusy;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
planner_running_ = false;
|
||||
|
||||
if (result.tag != plan_tag_)
|
||||
{
|
||||
// Kết quả của một yêu cầu đã bị thay hoặc huỷ. Vứt lặng lẽ: bám theo nó nghĩa là robot đi tới
|
||||
// goal không còn ai yêu cầu. Không phải lỗi, nên cũng không đặt kFailed.
|
||||
return;
|
||||
}
|
||||
|
||||
// Contract của PlannerPort là "succeeded nghĩa là plan không rỗng". Vẫn kiểm lại ở đây vì plan
|
||||
// rỗng lọt xuống sẽ thành front()/back() trên vector rỗng ở tầng dưới.
|
||||
if (!result.succeeded || result.plan.empty())
|
||||
{
|
||||
planner_feedback_ = PlannerFeedback::kFailed;
|
||||
return;
|
||||
}
|
||||
|
||||
latest_plan_.swap(result.plan);
|
||||
planner_feedback_ = PlannerFeedback::kPlanReady;
|
||||
}
|
||||
|
||||
void ControlLoop::runController(robot_geometry_msgs::Twist& candidate)
|
||||
{
|
||||
candidate = robot_geometry_msgs::Twist();
|
||||
|
||||
// Giữ nguyên thứ tự của interface được bọc: hỏi đã tới đích trước, chỉ khi chưa mới tính lệnh.
|
||||
if (deps_.controller->isGoalReached())
|
||||
{
|
||||
controller_feedback_ = ControllerFeedback::kGoalReached;
|
||||
return;
|
||||
}
|
||||
|
||||
robot_geometry_msgs::Twist cmd;
|
||||
if (deps_.controller->computeVelocityCommands(cmd))
|
||||
{
|
||||
controller_feedback_ = ControllerFeedback::kCommandValid;
|
||||
candidate = cmd;
|
||||
return;
|
||||
}
|
||||
|
||||
controller_feedback_ = ControllerFeedback::kNoValidCommand;
|
||||
}
|
||||
|
||||
bool ControlLoop::step()
|
||||
{
|
||||
if (!initialized_)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- 1. Thời gian và pose -------------------------------------------------------------------
|
||||
const robot::Time now = deps_.clock->now();
|
||||
|
||||
double dt = config_.nominal_control_period;
|
||||
if (has_last_cycle_time_)
|
||||
{
|
||||
dt = (now - last_cycle_time_).toSec();
|
||||
if (dt < 0.0)
|
||||
{
|
||||
// Đồng hồ lùi (thường do đổi nguồn time). Không có dt tin được thì bỏ giới hạn gia tốc ở
|
||||
// cycle này thay vì tính ra một giá trị bịa.
|
||||
dt = 0.0;
|
||||
}
|
||||
}
|
||||
last_cycle_time_ = now;
|
||||
has_last_cycle_time_ = true;
|
||||
|
||||
// Thu kết quả lập plan TRƯỚC khi dựng dữ liệu vào: state machine quyết định dựa trên phản hồi,
|
||||
// nên phản hồi phải có mặt trước lúc nó chạy.
|
||||
collectPlannerResult();
|
||||
|
||||
robot_geometry_msgs::PoseStamped robot_pose;
|
||||
const bool pose_available = deps_.pose->getRobotPose(robot_pose);
|
||||
|
||||
double travelled = 0.0;
|
||||
if (pose_available && has_oscillation_origin_)
|
||||
{
|
||||
const double dx = robot_pose.pose.position.x - oscillation_origin_.pose.position.x;
|
||||
const double dy = robot_pose.pose.position.y - oscillation_origin_.pose.position.y;
|
||||
travelled = std::hypot(dx, dy);
|
||||
}
|
||||
|
||||
// --- 2. State machine -----------------------------------------------------------------------
|
||||
StateMachineInput input;
|
||||
input.now = now;
|
||||
input.has_pending_request = has_pending_request_;
|
||||
input.pending_request_has_goal = pending_request_.has_goal;
|
||||
input.pending_request_action_count = pending_request_.actions.size();
|
||||
input.pause_requested = pause_requested_;
|
||||
input.resume_requested = resume_requested_;
|
||||
input.cancel_requested = cancel_requested_;
|
||||
input.planner = planner_feedback_;
|
||||
input.controller = controller_feedback_;
|
||||
input.recovery = recovery_feedback_;
|
||||
input.action = action_feedback_;
|
||||
input.pose_available = pose_available;
|
||||
input.robot_stopped = arbiter_.stopped();
|
||||
input.travelled_since_oscillation_reset = travelled;
|
||||
|
||||
// Cùng một chỉ số dùng cho cả cycle khởi động recovery lẫn các cycle tick: state machine chỉ tăng
|
||||
// chỉ số khi behavior kết thúc, nên `nextRecoveryIndex()` chính là behavior sắp chạy hoặc đang
|
||||
// chạy. Hỏi trước khi state machine quyết định để nó biết có nên trao quyền phát vận tốc không.
|
||||
input.active_recovery_output =
|
||||
deps_.recovery != nullptr
|
||||
? deps_.recovery->outputKind(state_machine_.nextRecoveryIndex())
|
||||
: RecoveryOutputKind::kNone;
|
||||
|
||||
const StateMachineOutput output = state_machine_.update(input);
|
||||
|
||||
if (output.state_changed)
|
||||
{
|
||||
last_reason_ = output.reason;
|
||||
}
|
||||
|
||||
// Cờ một-lần đã được state machine tiêu thụ xong ở lời gọi trên.
|
||||
pause_requested_ = false;
|
||||
resume_requested_ = false;
|
||||
|
||||
// Phản hồi của cycle trước đã dùng xong; đặt lại để cycle này tự sinh phản hồi mới.
|
||||
planner_feedback_ = PlannerFeedback::kIdle;
|
||||
controller_feedback_ = ControllerFeedback::kIdle;
|
||||
recovery_feedback_ = RecoveryFeedback::kIdle;
|
||||
action_feedback_ = ActionFeedback::kIdle;
|
||||
|
||||
// --- 3. Thi hành output ---------------------------------------------------------------------
|
||||
if (output.accept_request)
|
||||
{
|
||||
active_request_ = pending_request_;
|
||||
has_active_request_ = true;
|
||||
has_pending_request_ = false;
|
||||
latest_plan_.clear();
|
||||
has_outcome_ = false;
|
||||
|
||||
// Nhãn mới: mọi lượt lập plan đang bay thuộc về goal cũ và phải bị vứt khi về.
|
||||
++plan_tag_;
|
||||
deps_.planner->cancelPlan();
|
||||
planner_running_ = false;
|
||||
}
|
||||
|
||||
if (output.reset_oscillation_origin && pose_available)
|
||||
{
|
||||
oscillation_origin_ = robot_pose;
|
||||
has_oscillation_origin_ = true;
|
||||
}
|
||||
|
||||
if (output.stop_planner)
|
||||
{
|
||||
// Huỷ thật, không chỉ quên đi: nếu chỉ hạ cờ thì lượt đang chạy vẫn về và chiếm chỗ hộp thư,
|
||||
// rồi bị nhận nhầm cho lượt kế tiếp mang cùng nhãn.
|
||||
deps_.planner->cancelPlan();
|
||||
planner_running_ = false;
|
||||
}
|
||||
|
||||
if (output.apply_plan && !latest_plan_.empty())
|
||||
{
|
||||
if (!deps_.controller->setPlan(latest_plan_))
|
||||
{
|
||||
// Controller từ chối plan: coi như lần lập plan này hỏng, để chu kỳ kiên nhẫn tiếp tục chạy.
|
||||
planner_feedback_ = PlannerFeedback::kFailed;
|
||||
}
|
||||
}
|
||||
|
||||
if (output.cancel_recovery)
|
||||
{
|
||||
deps_.recovery->cancel();
|
||||
}
|
||||
|
||||
robot_geometry_msgs::Twist candidate;
|
||||
|
||||
if (output.start_recovery)
|
||||
{
|
||||
if (!deps_.recovery->start(output.recovery_index, output.recovery_trigger))
|
||||
{
|
||||
// Behavior từ chối khởi động (ví dụ đã va chạm ngay tại chỗ) — báo hỏng để state machine
|
||||
// chuyển sang behavior kế tiếp thay vì tick một behavior chưa start.
|
||||
recovery_feedback_ = RecoveryFeedback::kFailed;
|
||||
}
|
||||
}
|
||||
else if (output.tick_recovery)
|
||||
{
|
||||
const RecoveryTick tick = deps_.recovery->update();
|
||||
switch (tick.status)
|
||||
{
|
||||
case RecoveryTick::Status::kRunning:
|
||||
recovery_feedback_ = RecoveryFeedback::kRunning;
|
||||
break;
|
||||
case RecoveryTick::Status::kSucceeded:
|
||||
recovery_feedback_ = RecoveryFeedback::kSucceeded;
|
||||
break;
|
||||
case RecoveryTick::Status::kFailed:
|
||||
recovery_feedback_ = RecoveryFeedback::kFailed;
|
||||
break;
|
||||
}
|
||||
if (tick.has_velocity)
|
||||
{
|
||||
candidate = tick.cmd;
|
||||
}
|
||||
if (tick.has_path && !tick.path.empty())
|
||||
{
|
||||
// Họ recovery sinh lại đường đi: coi kết quả như một plan mới, chờ state machine đẩy xuống.
|
||||
latest_plan_ = tick.path;
|
||||
planner_feedback_ = PlannerFeedback::kPlanReady;
|
||||
}
|
||||
}
|
||||
|
||||
if (output.cancel_action && deps_.action != nullptr)
|
||||
{
|
||||
deps_.action->cancel();
|
||||
}
|
||||
|
||||
if (output.start_action)
|
||||
{
|
||||
// Cửa submit đã chặn yêu cầu có action mà không có port, nhưng vẫn guard: kẹt ở đây nghĩa là
|
||||
// contract bị phá từ một đường khác — báo action hỏng để state machine kết thúc tường minh.
|
||||
if (deps_.action == nullptr || output.action_index >= active_request_.actions.size())
|
||||
{
|
||||
action_feedback_ = ActionFeedback::kFailed;
|
||||
}
|
||||
else if (!deps_.action->start(active_request_.actions[output.action_index]))
|
||||
{
|
||||
// Không có handler cho actionType này hoặc handler từ chối — action coi như thất bại.
|
||||
action_feedback_ = ActionFeedback::kFailed;
|
||||
}
|
||||
}
|
||||
else if (output.tick_action && deps_.action != nullptr)
|
||||
{
|
||||
const ActionTick tick = deps_.action->update();
|
||||
switch (tick.status)
|
||||
{
|
||||
case ActionTick::Status::kRunning:
|
||||
action_feedback_ = ActionFeedback::kRunning;
|
||||
break;
|
||||
case ActionTick::Status::kSucceeded:
|
||||
action_feedback_ = ActionFeedback::kSucceeded;
|
||||
break;
|
||||
case ActionTick::Status::kFailed:
|
||||
action_feedback_ = ActionFeedback::kFailed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (output.run_controller)
|
||||
{
|
||||
runController(candidate);
|
||||
}
|
||||
|
||||
if (output.start_planner && pose_available && !planner_running_)
|
||||
{
|
||||
// `start_planner` là tín hiệu MỨC ("hãy đang lập plan"), bật lại mỗi cycle chừng nào state
|
||||
// machine còn ở kPlanning — không phải sườn. Kick lại một lượt đang chạy sẽ vừa bị cổng từ
|
||||
// chối, vừa làm mất thời gian đã bỏ ra.
|
||||
planner_running_ = deps_.planner->startPlan(robot_pose, active_request_.goal,
|
||||
active_request_.order.get(), plan_tag_);
|
||||
if (!planner_running_)
|
||||
{
|
||||
// Không khởi động được (chưa có planner, pose hỏng...). Coi như một lượt hỏng để đồng hồ
|
||||
// kiên nhẫn tiếp tục chạy, thay vì đứng im ở kPlanning vô hạn.
|
||||
planner_feedback_ = PlannerFeedback::kFailed;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 4. Lệnh vận tốc ------------------------------------------------------------------------
|
||||
arbiter_.arbitrate(output.velocity_source, candidate, dt);
|
||||
|
||||
// --- 5. Báo kết quả -------------------------------------------------------------------------
|
||||
if (output.report_outcome)
|
||||
{
|
||||
last_outcome_ = output.outcome;
|
||||
has_outcome_ = true;
|
||||
++outcome_report_count_;
|
||||
|
||||
if (deps_.mission != nullptr && has_active_request_ &&
|
||||
active_request_.mission_sequence_id != 0)
|
||||
{
|
||||
deps_.mission->reportOutcome(active_request_.mission_sequence_id, output.outcome);
|
||||
}
|
||||
|
||||
has_active_request_ = false;
|
||||
cancel_requested_ = false;
|
||||
latest_plan_.clear();
|
||||
planner_running_ = false;
|
||||
}
|
||||
|
||||
return !output.report_outcome;
|
||||
}
|
||||
|
||||
const char* ControlLoop::lastOutcome() const
|
||||
{
|
||||
return has_outcome_ ? toString(last_outcome_) : "";
|
||||
}
|
||||
|
||||
} // namespace move_base2
|
||||
337
src/io/sensor_gateway.cpp
Normal file
337
src/io/sensor_gateway.cpp
Normal file
@@ -0,0 +1,337 @@
|
||||
/*********************************************************************
|
||||
*
|
||||
* Software License Agreement (BSD License)
|
||||
*
|
||||
* move_base2 — cài đặt cửa vào dữ liệu cảm biến.
|
||||
*
|
||||
* Author: DuongTD
|
||||
*********************************************************************/
|
||||
#include <move_base2/io/sensor_gateway.h>
|
||||
|
||||
#include <exception>
|
||||
#include <sstream>
|
||||
|
||||
#include <laser_filter/laser_filter.h>
|
||||
#include <robot/console.h>
|
||||
#include <robot_costmap_2d/layer.h>
|
||||
#include <robot_costmap_2d/layered_costmap.h>
|
||||
|
||||
namespace move_base2
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
/// [s] Giãn cách log cho các cảnh báo phát sinh trong đường nóng — tần số cảm biến, không được spam.
|
||||
constexpr double kHotPathLogThrottle = 5.0;
|
||||
|
||||
const char* toString(robot_costmap_2d::LayerType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case robot_costmap_2d::LayerType::STATIC_LAYER:
|
||||
return "StaticLayer";
|
||||
case robot_costmap_2d::LayerType::OBSTACLE_LAYER:
|
||||
return "ObstacleLayer";
|
||||
case robot_costmap_2d::LayerType::VOXEL_LAYER:
|
||||
return "VoxelLayer";
|
||||
case robot_costmap_2d::LayerType::INFLATION_LAYER:
|
||||
return "InflationLayer";
|
||||
case robot_costmap_2d::LayerType::CRITICAL_LAYER:
|
||||
return "CriticalLayer";
|
||||
case robot_costmap_2d::LayerType::DIRECTIONAL_LAYER:
|
||||
return "DirectionalLayer";
|
||||
case robot_costmap_2d::LayerType::PREFERRED_LAYER:
|
||||
return "PreferredLayer";
|
||||
case robot_costmap_2d::LayerType::UNPREFERRED_LAYER:
|
||||
return "UnpreferredLayer";
|
||||
case robot_costmap_2d::LayerType::UNKNOWN:
|
||||
break;
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ================================================================================================
|
||||
// SensorGatewayConfig
|
||||
// ================================================================================================
|
||||
|
||||
bool SensorGatewayConfig::validate(std::string& error) const
|
||||
{
|
||||
// Chỉ kiểm khi lọc bật: tham số của một tính năng đang tắt không nên chặn được cả runtime.
|
||||
if (!laser_sor_enabled)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (laser_sor_mean_k < 2)
|
||||
{
|
||||
error = "laser_sor_mean_k phải >= 2 [điểm] khi laser_sor_enabled = true";
|
||||
return false;
|
||||
}
|
||||
if (!(laser_sor_stddev_mul > 0.0))
|
||||
{
|
||||
error = "laser_sor_stddev_mul phải > 0 khi laser_sor_enabled = true";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string SensorGatewayConfig::describe() const
|
||||
{
|
||||
std::ostringstream out;
|
||||
out << "SensorGateway:\n";
|
||||
out << " laser_sor_enabled : " << (laser_sor_enabled ? "true" : "false") << '\n';
|
||||
if (laser_sor_enabled)
|
||||
{
|
||||
out << " laser_sor_mean_k : " << laser_sor_mean_k << " điểm\n";
|
||||
out << " laser_sor_stddev_mul : " << laser_sor_stddev_mul << '\n';
|
||||
}
|
||||
return out.str();
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// SensorGateway
|
||||
// ================================================================================================
|
||||
|
||||
SensorGateway::SensorGateway() = default;
|
||||
|
||||
// Định nghĩa ở đây (không phải `= default` trong header) vì laser_sor_ là unique_ptr tới kiểu chưa
|
||||
// hoàn chỉnh ở phía header.
|
||||
SensorGateway::~SensorGateway() = default;
|
||||
|
||||
bool SensorGateway::configure(const SensorGatewayConfig& config, std::string& error)
|
||||
{
|
||||
if (!config.validate(error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
config_ = config;
|
||||
|
||||
if (config_.laser_sor_enabled)
|
||||
{
|
||||
// Dựng một lần, không phải mỗi mẫu như bản cũ. Tham số cũng chỉ set ở đây.
|
||||
laser_sor_.reset(new laser_filter::LaserScanSOR());
|
||||
laser_sor_->setMeanK(config_.laser_sor_mean_k);
|
||||
laser_sor_->setStddevMulThresh(config_.laser_sor_stddev_mul);
|
||||
}
|
||||
else
|
||||
{
|
||||
laser_sor_.reset();
|
||||
}
|
||||
|
||||
configured_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void SensorGateway::attach(robot_costmap_2d::LayeredCostmap* global,
|
||||
robot_costmap_2d::LayeredCostmap* local)
|
||||
{
|
||||
global_costmap_ = global;
|
||||
local_costmap_ = local;
|
||||
|
||||
warnAboutUnreachableLayers(global_costmap_, "global");
|
||||
warnAboutUnreachableLayers(local_costmap_, "local");
|
||||
}
|
||||
|
||||
bool SensorGateway::attached() const
|
||||
{
|
||||
return global_costmap_ != nullptr || local_costmap_ != nullptr;
|
||||
}
|
||||
|
||||
void SensorGateway::warnAboutUnreachableLayers(robot_costmap_2d::LayeredCostmap* costmap,
|
||||
const char* which) const
|
||||
{
|
||||
if (costmap == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto* plugins = costmap->getPlugins();
|
||||
if (plugins == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Dữ liệu vật cản được đẩy theo LayerType::VOXEL_LAYER, đúng như bản cũ. Một layer khai
|
||||
// `type: ObstacleLayer` thuần sẽ không bao giờ khớp và không nhận được gì — im lặng. Đây là bẫy
|
||||
// có thật: cây config `robot_costmap_2d/config/costmap_params.yaml` đang khai đúng kiểu đó.
|
||||
// Không tự ý mở rộng đích để tránh đổi hành vi; thay vào đó nói ra lúc khởi tạo.
|
||||
for (const auto& layer : *plugins)
|
||||
{
|
||||
if (!layer)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (layer->getType() == robot_costmap_2d::LayerType::OBSTACLE_LAYER)
|
||||
{
|
||||
robot::log_warning(
|
||||
"[SensorGateway] costmap %s: layer '%s' kiểu ObstacleLayer sẽ KHÔNG nhận dữ liệu cảm "
|
||||
"biến — cổng này đẩy vật cản theo LayerType::VOXEL_LAYER. Đổi sang 'type: VoxelLayer' "
|
||||
"trong danh sách plugins nếu layer đó cần dữ liệu.\n",
|
||||
which, layer->getName().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
robot_sensor_msgs::LaserScan SensorGateway::prepareLaserScan(
|
||||
const robot_sensor_msgs::LaserScan& scan) const
|
||||
{
|
||||
if (!config_.laser_sor_enabled || laser_sor_ == nullptr)
|
||||
{
|
||||
return scan;
|
||||
}
|
||||
return laser_sor_->filter(scan);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Đưa một mẫu tới mọi layer khớp kiểu trong một costmap.
|
||||
*
|
||||
* Template nằm trong .cpp có chủ đích: nó là chỗ duy nhất chạm `dataCallBack`, và giữ nó ở đây làm
|
||||
* cho `sensor_gateway.h` không phải kéo theo `robot_costmap_2d`.
|
||||
*/
|
||||
template <typename T>
|
||||
void dispatchTo(robot_costmap_2d::LayeredCostmap* costmap, const T& value,
|
||||
robot_costmap_2d::LayerType type, const std::string& name,
|
||||
SensorGatewayStats& stats)
|
||||
{
|
||||
if (costmap == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto* plugins = costmap->getPlugins();
|
||||
if (plugins == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& layer : *plugins)
|
||||
{
|
||||
if (!layer)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Lọc CHỈ theo kiểu. Vế `|| getName() == name` của bản cũ bị bỏ — xem doc của lớp.
|
||||
if (layer->getType() != type)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!layer->isEnabled())
|
||||
{
|
||||
++stats.skipped_disabled;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
layer->dataCallBack<T>(value, name);
|
||||
++stats.delivered;
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
// Bắt quanh TỪNG layer: bản cũ bắt quanh cả vòng lặp rồi return, nên một layer hỏng làm mọi
|
||||
// layer đứng sau nó mất luôn mẫu này.
|
||||
++stats.layer_exceptions;
|
||||
robot::log_error_throttle(
|
||||
kHotPathLogThrottle,
|
||||
"[SensorGateway] layer '%s' (%s) ném exception khi nhận '%s': %s\n",
|
||||
layer->getName().c_str(), toString(type), name.c_str(), ex.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SensorGateway::pushStaticMap(const std::string& name, const robot_nav_msgs::OccupancyGrid& map)
|
||||
{
|
||||
if (!attached())
|
||||
{
|
||||
++stats_.dropped_no_costmap;
|
||||
robot::log_warning_throttle(kHotPathLogThrottle,
|
||||
"[SensorGateway] bỏ static map '%s': chưa gắn costmap nào\n",
|
||||
name.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
dispatchTo(global_costmap_, map, robot_costmap_2d::LayerType::STATIC_LAYER, name, stats_);
|
||||
dispatchTo(local_costmap_, map, robot_costmap_2d::LayerType::STATIC_LAYER, name, stats_);
|
||||
}
|
||||
|
||||
void SensorGateway::pushLaserScan(const std::string& name, const robot_sensor_msgs::LaserScan& scan)
|
||||
{
|
||||
if (!attached())
|
||||
{
|
||||
++stats_.dropped_no_costmap;
|
||||
robot::log_warning_throttle(kHotPathLogThrottle,
|
||||
"[SensorGateway] bỏ laser scan '%s': chưa gắn costmap nào\n",
|
||||
name.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
dispatchTo(local_costmap_, scan, robot_costmap_2d::LayerType::VOXEL_LAYER, name, stats_);
|
||||
dispatchTo(global_costmap_, scan, robot_costmap_2d::LayerType::VOXEL_LAYER, name, stats_);
|
||||
}
|
||||
|
||||
void SensorGateway::pushPointCloud(const std::string& name,
|
||||
const robot_sensor_msgs::PointCloud& cloud)
|
||||
{
|
||||
if (!attached())
|
||||
{
|
||||
++stats_.dropped_no_costmap;
|
||||
robot::log_warning_throttle(kHotPathLogThrottle,
|
||||
"[SensorGateway] bỏ point cloud '%s': chưa gắn costmap nào\n",
|
||||
name.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
dispatchTo(local_costmap_, cloud, robot_costmap_2d::LayerType::VOXEL_LAYER, name, stats_);
|
||||
dispatchTo(global_costmap_, cloud, robot_costmap_2d::LayerType::VOXEL_LAYER, name, stats_);
|
||||
}
|
||||
|
||||
void SensorGateway::pushPointCloud2(const std::string& name,
|
||||
const robot_sensor_msgs::PointCloud2& cloud)
|
||||
{
|
||||
if (!attached())
|
||||
{
|
||||
++stats_.dropped_no_costmap;
|
||||
robot::log_warning_throttle(kHotPathLogThrottle,
|
||||
"[SensorGateway] bỏ point cloud2 '%s': chưa gắn costmap nào\n",
|
||||
name.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
dispatchTo(local_costmap_, cloud, robot_costmap_2d::LayerType::VOXEL_LAYER, name, stats_);
|
||||
dispatchTo(global_costmap_, cloud, robot_costmap_2d::LayerType::VOXEL_LAYER, name, stats_);
|
||||
}
|
||||
|
||||
void SensorGateway::pushDepthCameraData(const std::string& topic,
|
||||
const robot_sensor_msgs::DepthCameraData::ConstPtr& data)
|
||||
{
|
||||
if (data == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!attached())
|
||||
{
|
||||
++stats_.dropped_no_costmap;
|
||||
robot::log_warning_throttle(kHotPathLogThrottle,
|
||||
"[SensorGateway] bỏ depth camera '%s': chưa gắn costmap nào\n",
|
||||
topic.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Phải giữ nguyên dạng ConstPtr: layer so `typeid(DepthCameraData::ConstPtr)`. Truyền giá trị sẽ
|
||||
// rơi im lặng qua mọi nhánh của handleImpl.
|
||||
dispatchTo(local_costmap_, data, robot_costmap_2d::LayerType::VOXEL_LAYER, topic, stats_);
|
||||
dispatchTo(global_costmap_, data, robot_costmap_2d::LayerType::VOXEL_LAYER, topic, stats_);
|
||||
}
|
||||
|
||||
} // namespace move_base2
|
||||
35
src/move_base2_plugin.cpp
Normal file
35
src/move_base2_plugin.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
/*********************************************************************
|
||||
*
|
||||
* Software License Agreement (BSD License)
|
||||
*
|
||||
* move_base2 — export plugin Boost.DLL.
|
||||
*
|
||||
* Author: DuongTD
|
||||
*********************************************************************/
|
||||
#include <memory>
|
||||
|
||||
#include <boost/dll/alias.hpp>
|
||||
|
||||
#include <move_base_core/navigation.h>
|
||||
|
||||
#include <move_base2/navigation_server.h>
|
||||
|
||||
namespace move_base2
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Factory được host nạp qua boost::dll::import_alias.
|
||||
*
|
||||
* Kiểu trả về phải khớp CHÍNH XÁC kiểu mà loader khai báo:
|
||||
* `robot::move_base_core::BaseNavigation::Ptr()`. Cơ chế nạp là dlsym + reinterpret_cast, không có
|
||||
* kiểm kiểu nào qua ranh giới .so — lệch kiểu ở đây không gây lỗi biên dịch mà gây hỏng bộ nhớ lúc
|
||||
* chạy. Đổi contract thì phải đổi cả loader trong cùng một lần sửa.
|
||||
*/
|
||||
robot::move_base_core::BaseNavigation::Ptr createMoveBase2()
|
||||
{
|
||||
return std::make_shared<NavigationServer>();
|
||||
}
|
||||
|
||||
} // namespace move_base2
|
||||
|
||||
BOOST_DLL_ALIAS(move_base2::createMoveBase2, MoveBase2)
|
||||
653
src/navigation_server.cpp
Normal file
653
src/navigation_server.cpp
Normal file
@@ -0,0 +1,653 @@
|
||||
/*********************************************************************
|
||||
*
|
||||
* Software License Agreement (BSD License)
|
||||
*
|
||||
* move_base2 — cài đặt facade BaseNavigation.
|
||||
*
|
||||
* Author: DuongTD
|
||||
*********************************************************************/
|
||||
#include <move_base2/navigation_server.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <robot_nav_2d_utils/conversions.h>
|
||||
|
||||
namespace move_base2
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
/// @brief Lấy phần tử theo khoá, trả về giá trị mặc định nếu không có.
|
||||
template <typename MapT>
|
||||
typename MapT::mapped_type lookupOrDefault(const MapT& container,
|
||||
const typename MapT::key_type& key)
|
||||
{
|
||||
const auto it = container.find(key);
|
||||
return it == container.end() ? typename MapT::mapped_type() : it->second;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NavigationServer::NavigationServer()
|
||||
{
|
||||
// nav_feedback_ là thành viên của contract host và được host đọc qua con trỏ, nên phải tồn tại
|
||||
// ngay từ lúc dựng — trước cả initialize().
|
||||
nav_feedback_ = std::make_shared<robot::move_base_core::NavFeedback>();
|
||||
nav_feedback_->navigation_state = robot::move_base_core::State::PENDING;
|
||||
nav_feedback_->feed_back_str = "chưa khởi tạo";
|
||||
nav_feedback_->goal_checked = false;
|
||||
nav_feedback_->is_ready = false;
|
||||
}
|
||||
|
||||
NavigationServer::~NavigationServer() = default;
|
||||
|
||||
// ================================================================================================
|
||||
// Cấu hình lõi
|
||||
// ================================================================================================
|
||||
|
||||
bool NavigationServer::configureLoop(const ControlLoopConfig& config, const ControlLoopDeps& deps,
|
||||
std::string& error)
|
||||
{
|
||||
if (!loop_.configure(config, deps, error))
|
||||
{
|
||||
nav_feedback_->is_ready = false;
|
||||
nav_feedback_->feed_back_str = "cấu hình lỗi: " + error;
|
||||
return false;
|
||||
}
|
||||
|
||||
robot_base_frame_ = config.robot_base_frame;
|
||||
|
||||
nav_feedback_->is_ready = true;
|
||||
nav_feedback_->feed_back_str = "sẵn sàng";
|
||||
refreshFeedback();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NavigationServer::configureSensors(const SensorGatewayConfig& config, std::string& error)
|
||||
{
|
||||
return sensors_.configure(config, error);
|
||||
}
|
||||
|
||||
void NavigationServer::attachCostmaps(robot_costmap_2d::LayeredCostmap* global,
|
||||
robot_costmap_2d::LayeredCostmap* local)
|
||||
{
|
||||
sensors_.attach(global, local);
|
||||
|
||||
// Phát lại static map đã nhận trước khi costmap tồn tại. Chỉ static map, cố ý: một laser scan cũ
|
||||
// phát lại vào costmap là dựng vật cản ở chỗ robot có thể đã rời khỏi từ lâu — im lặng và nguy
|
||||
// hiểm hơn hẳn việc chờ mẫu kế tiếp, vốn chỉ cách vài chục ms.
|
||||
std::map<std::string, robot_nav_msgs::OccupancyGrid> maps;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
maps = static_maps_;
|
||||
|
||||
// `map_save_`/`map_name_save_` là cặp biến PUBLIC của BaseNavigation mà host tự gán (đường bù
|
||||
// của bản cũ cho đúng vấn đề này). Tôn trọng nó để host không phải sửa gì, nhưng không để nó
|
||||
// ghi đè bản đã đi qua addStaticMap.
|
||||
if (!map_name_save_.empty() && maps.find(map_name_save_) == maps.end())
|
||||
{
|
||||
maps[map_name_save_] = map_save_;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& entry : maps)
|
||||
{
|
||||
sensors_.pushStaticMap(entry.first, entry.second);
|
||||
}
|
||||
}
|
||||
|
||||
bool NavigationServer::spinOnce()
|
||||
{
|
||||
// Trước khi tính lệnh: đẩy xuống controller những gì host đã đặt từ thread của nó. Đặt ở đây chứ
|
||||
// không ở cuối cycle để trần vận tốc có hiệu lực ngay trong chính cycle này — chậm một cycle
|
||||
// nghĩa là một chu kỳ nữa chạy quá tốc độ mà tầng an toàn vừa yêu cầu hạ.
|
||||
pushHostInputsToController();
|
||||
|
||||
const bool running = loop_.step();
|
||||
publishCommand();
|
||||
refreshFeedback();
|
||||
return running;
|
||||
}
|
||||
|
||||
void NavigationServer::publishCommand()
|
||||
{
|
||||
// getTwist() của contract host là LỆNH vận tốc đang phát, không phải vận tốc đo được: host lấy nó
|
||||
// rồi publish thẳng ra cmd_vel (amr_publiser.cpp:360-370). Đổ odometry vào đây tạo vòng lặp dương
|
||||
// — robot giữ nguyên tốc độ hiện tại vô hạn và VelocityArbiter bị vô hiệu hoàn toàn.
|
||||
//
|
||||
// Nguồn duy nhất đúng là lệnh vừa qua bộ trọng tài. Dấu thời gian lấy theo cycle của control loop
|
||||
// chứ không phải giờ hệ thống lúc gọi: host loại lệnh quá hạn, nên control loop treo phải làm dấu
|
||||
// thời gian đứng yên để host thấy được và ngừng phát.
|
||||
const robot_geometry_msgs::Twist& command = loop_.lastCommand();
|
||||
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
twist_.velocity = robot_nav_2d_utils::twist3Dto2D(command);
|
||||
twist_.header.stamp = loop_.lastCycleTime();
|
||||
twist_.header.frame_id = robot_base_frame_;
|
||||
}
|
||||
|
||||
robot::move_base_core::State NavigationServer::toHostState(NavigationState state)
|
||||
{
|
||||
using HostState = robot::move_base_core::State;
|
||||
switch (state)
|
||||
{
|
||||
case NavigationState::kIdle:
|
||||
return HostState::PENDING;
|
||||
case NavigationState::kPlanning:
|
||||
return HostState::PLANNING;
|
||||
case NavigationState::kControlling:
|
||||
return HostState::CONTROLLING;
|
||||
case NavigationState::kRecovering:
|
||||
// Contract host chỉ có CLEARING cho giai đoạn phục hồi. Ánh xạ về đó để host cũ không phải
|
||||
// đổi gì; ngữ nghĩa mới (recovery có thời lượng, có thể phát vận tốc) nằm ở phía lõi.
|
||||
return HostState::CLEARING;
|
||||
case NavigationState::kExecutingActions:
|
||||
// Contract host không có khái niệm action (D8 mới thêm). ACTIVE (actionlib: goal đang được
|
||||
// xử lý) là ánh xạ đúng: giữ nghĩa "chặng chưa xong" nhưng KHÔNG phải CONTROLLING — host
|
||||
// VDA5050 đang suy `driving = true` từ CONTROLLING (amr_vda_5050_client_api.cpp:1233), mà
|
||||
// robot lúc này đứng yên làm action; báo "đang chạy" cho fleet master là báo sai.
|
||||
return HostState::ACTIVE;
|
||||
case NavigationState::kPaused:
|
||||
return HostState::PAUSED;
|
||||
case NavigationState::kCancelling:
|
||||
return HostState::PREEMPTING;
|
||||
case NavigationState::kSucceeded:
|
||||
return HostState::SUCCEEDED;
|
||||
case NavigationState::kAborted:
|
||||
return HostState::ABORTED;
|
||||
case NavigationState::kCancelled:
|
||||
return HostState::PREEMPTED;
|
||||
}
|
||||
return HostState::LOST;
|
||||
}
|
||||
|
||||
void NavigationServer::refreshFeedback()
|
||||
{
|
||||
nav_feedback_->navigation_state = toHostState(loop_.state());
|
||||
|
||||
const char* reason = loop_.lastReason();
|
||||
if (reason != nullptr && reason[0] != '\0')
|
||||
{
|
||||
nav_feedback_->feed_back_str = reason;
|
||||
}
|
||||
|
||||
robot_geometry_msgs::Pose2D pose2d;
|
||||
nav_feedback_->goal_checked = getRobotPose(pose2d);
|
||||
if (nav_feedback_->goal_checked)
|
||||
{
|
||||
nav_feedback_->current_pose = pose2d;
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// Khởi tạo
|
||||
// ================================================================================================
|
||||
|
||||
void NavigationServer::initialize(robot::TFListenerPtr tf)
|
||||
{
|
||||
tf_ = tf;
|
||||
|
||||
// Phần dựng costmap, planner runner, controller runner và recovery runner từ tf này thuộc bước
|
||||
// nối dây runtime. Cho tới lúc đó, các cổng phải được bơm vào qua configureLoop() — cố ý KHÔNG
|
||||
// tự dựng cổng giả ở đây, vì một runtime chạy được với cổng giả là thứ nguy hiểm nhất có thể có.
|
||||
if (!loop_.initialized())
|
||||
{
|
||||
nav_feedback_->is_ready = false;
|
||||
nav_feedback_->feed_back_str = "đã nhận tf, chờ configureLoop() nạp các cổng runtime";
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// Footprint
|
||||
// ================================================================================================
|
||||
|
||||
void NavigationServer::setRobotFootprint(const std::vector<robot_geometry_msgs::Point>& fprt)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
footprint_ = fprt;
|
||||
}
|
||||
|
||||
std::vector<robot_geometry_msgs::Point> NavigationServer::getRobotFootprint()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
return footprint_;
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// Nhận dữ liệu sensor
|
||||
// ================================================================================================
|
||||
|
||||
// Khuôn chung của cả năm hàm dưới đây: cất giữ dưới `data_mutex_`, ĐÓNG lock, rồi mới đẩy vào
|
||||
// costmap. Thứ tự đó là bắt buộc chứ không phải phong cách — `StaticLayer::incomingMap` có thể gọi
|
||||
// `LayeredCostmap::resizeMap`, hàm này chờ mutex master costmap và có thể đứng trọn một chu kỳ
|
||||
// `updateMap`. Đẩy trong lúc còn giữ `data_mutex_` sẽ kéo theo `getTwist`/`getRobotFootprint`/
|
||||
// `getStaticMap` của host chết chờ cùng, trong khi host chỉ có MỘT thread phục vụ mọi callback.
|
||||
|
||||
void NavigationServer::addStaticMap(const std::string& map_name, robot_nav_msgs::OccupancyGrid map)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
static_maps_[map_name] = map;
|
||||
}
|
||||
sensors_.pushStaticMap(map_name, map);
|
||||
}
|
||||
|
||||
void NavigationServer::addLaserScan(const std::string& laser_scan_name,
|
||||
robot_sensor_msgs::LaserScan laser_scan)
|
||||
{
|
||||
// Lọc TRƯỚC khi cất: bản được cất và bản costmap nhìn thấy phải là một. Bản cũ cũng cất bản đã
|
||||
// lọc; nếu getter trả bản thô còn costmap thấy bản lọc thì hai nguồn sự thật sẽ lệch nhau.
|
||||
const robot_sensor_msgs::LaserScan prepared = sensors_.prepareLaserScan(laser_scan);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
laser_scans_[laser_scan_name] = prepared;
|
||||
}
|
||||
sensors_.pushLaserScan(laser_scan_name, prepared);
|
||||
}
|
||||
|
||||
void NavigationServer::addPointCloud(const std::string& point_cloud_name,
|
||||
robot_sensor_msgs::PointCloud point_cloud)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
point_clouds_[point_cloud_name] = point_cloud;
|
||||
}
|
||||
sensors_.pushPointCloud(point_cloud_name, point_cloud);
|
||||
}
|
||||
|
||||
void NavigationServer::addPointCloud2(const std::string& point_cloud2_name,
|
||||
robot_sensor_msgs::PointCloud2 point_cloud2)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
point_cloud2s_[point_cloud2_name] = point_cloud2;
|
||||
}
|
||||
sensors_.pushPointCloud2(point_cloud2_name, point_cloud2);
|
||||
}
|
||||
|
||||
void NavigationServer::addDepthCameraData(const std::string& topic,
|
||||
robot_sensor_msgs::DepthCameraData::ConstPtr data)
|
||||
{
|
||||
if (data == nullptr)
|
||||
{
|
||||
return; // Bỏ mẫu null thay vì cất một con trỏ rỗng để tầng sau vấp phải.
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
depth_camera_data_[topic] = data;
|
||||
}
|
||||
sensors_.pushDepthCameraData(topic, data);
|
||||
}
|
||||
|
||||
void NavigationServer::addOdometry(const std::string& /*odometry_name*/,
|
||||
robot_nav_msgs::Odometry odometry)
|
||||
{
|
||||
// CHỈ cất odometry. Không đụng twist_: xem publishCommand() — twist_ là lệnh phát ra, còn đây là
|
||||
// vận tốc đo được. Trộn hai thứ đó là đưa cảm biến vào thẳng đường lệnh.
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
odometry_ = std::move(odometry);
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// Đọc dữ liệu sensor
|
||||
// ================================================================================================
|
||||
|
||||
robot_nav_msgs::OccupancyGrid NavigationServer::getStaticMap(const std::string& map_name)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
return lookupOrDefault(static_maps_, map_name);
|
||||
}
|
||||
|
||||
robot_sensor_msgs::LaserScan NavigationServer::getLaserScan(const std::string& laser_scan_name)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
return lookupOrDefault(laser_scans_, laser_scan_name);
|
||||
}
|
||||
|
||||
robot_sensor_msgs::PointCloud NavigationServer::getPointCloud(const std::string& point_cloud_name)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
return lookupOrDefault(point_clouds_, point_cloud_name);
|
||||
}
|
||||
|
||||
robot_sensor_msgs::PointCloud2 NavigationServer::getPointCloud2(
|
||||
const std::string& point_cloud2_name)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
return lookupOrDefault(point_cloud2s_, point_cloud2_name);
|
||||
}
|
||||
|
||||
std::map<std::string, robot_nav_msgs::OccupancyGrid> NavigationServer::getAllStaticMaps()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
return static_maps_;
|
||||
}
|
||||
|
||||
std::map<std::string, robot_sensor_msgs::LaserScan> NavigationServer::getAllLaserScans()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
return laser_scans_;
|
||||
}
|
||||
|
||||
std::map<std::string, robot_sensor_msgs::PointCloud> NavigationServer::getAllPointClouds()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
return point_clouds_;
|
||||
}
|
||||
|
||||
std::map<std::string, robot_sensor_msgs::PointCloud2> NavigationServer::getAllPointCloud2s()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
return point_cloud2s_;
|
||||
}
|
||||
|
||||
bool NavigationServer::removeStaticMap(const std::string& map_name)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
return static_maps_.erase(map_name) > 0;
|
||||
}
|
||||
|
||||
bool NavigationServer::removeLaserScan(const std::string& laser_scan_name)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
return laser_scans_.erase(laser_scan_name) > 0;
|
||||
}
|
||||
|
||||
bool NavigationServer::removePointCloud(const std::string& point_cloud_name)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
return point_clouds_.erase(point_cloud_name) > 0;
|
||||
}
|
||||
|
||||
bool NavigationServer::removePointCloud2(const std::string& point_cloud2_name)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
return point_cloud2s_.erase(point_cloud2_name) > 0;
|
||||
}
|
||||
|
||||
bool NavigationServer::removeAllStaticMaps()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
static_maps_.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NavigationServer::removeAllLaserScans()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
laser_scans_.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NavigationServer::removeAllPointClouds()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
point_clouds_.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NavigationServer::removeAllPointCloud2s()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
point_cloud2s_.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NavigationServer::removeAllData()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
static_maps_.clear();
|
||||
laser_scans_.clear();
|
||||
point_clouds_.clear();
|
||||
point_cloud2s_.clear();
|
||||
depth_camera_data_.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// Sáu entry point di chuyển -> một NavigationRequest
|
||||
// ================================================================================================
|
||||
|
||||
bool NavigationServer::submit(const NavigationRequest& request)
|
||||
{
|
||||
std::string reason;
|
||||
if (loop_.submit(request, reason))
|
||||
{
|
||||
last_reject_reason_.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
last_reject_reason_ = reason;
|
||||
nav_feedback_->feed_back_str = "từ chối yêu cầu: " + reason;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool NavigationServer::moveTo(const robot_geometry_msgs::PoseStamped& goal,
|
||||
double xy_goal_tolerance, double yaw_goal_tolerance)
|
||||
{
|
||||
NavigationRequest request;
|
||||
request.profile = MotionProfile::kPosition;
|
||||
request.goal = goal;
|
||||
request.tolerance.xy = xy_goal_tolerance;
|
||||
request.tolerance.yaw = yaw_goal_tolerance;
|
||||
return submit(request);
|
||||
}
|
||||
|
||||
bool NavigationServer::moveTo(const robot_protocol_msgs::Order& msg,
|
||||
const robot_geometry_msgs::PoseStamped& goal,
|
||||
double xy_goal_tolerance, double yaw_goal_tolerance)
|
||||
{
|
||||
NavigationRequest request;
|
||||
request.profile = MotionProfile::kPosition;
|
||||
request.goal = goal;
|
||||
request.tolerance.xy = xy_goal_tolerance;
|
||||
request.tolerance.yaw = yaw_goal_tolerance;
|
||||
request.order = std::make_shared<robot_protocol_msgs::Order>(msg);
|
||||
return submit(request);
|
||||
}
|
||||
|
||||
bool NavigationServer::dockTo(const std::string& maker,
|
||||
const robot_geometry_msgs::PoseStamped& goal,
|
||||
double xy_goal_tolerance, double yaw_goal_tolerance)
|
||||
{
|
||||
NavigationRequest request;
|
||||
request.profile = MotionProfile::kDocking;
|
||||
request.goal = goal;
|
||||
request.tolerance.xy = xy_goal_tolerance;
|
||||
request.tolerance.yaw = yaw_goal_tolerance;
|
||||
request.marker = maker;
|
||||
return submit(request);
|
||||
}
|
||||
|
||||
bool NavigationServer::dockTo(const robot_protocol_msgs::Order& msg, const std::string& marker,
|
||||
const robot_geometry_msgs::PoseStamped& goal,
|
||||
double xy_goal_tolerance, double yaw_goal_tolerance)
|
||||
{
|
||||
NavigationRequest request;
|
||||
request.profile = MotionProfile::kDocking;
|
||||
request.goal = goal;
|
||||
request.tolerance.xy = xy_goal_tolerance;
|
||||
request.tolerance.yaw = yaw_goal_tolerance;
|
||||
request.marker = marker;
|
||||
request.order = std::make_shared<robot_protocol_msgs::Order>(msg);
|
||||
return submit(request);
|
||||
}
|
||||
|
||||
bool NavigationServer::moveStraightTo(const robot_geometry_msgs::PoseStamped& goal,
|
||||
double xy_goal_tolerance)
|
||||
{
|
||||
NavigationRequest request;
|
||||
request.profile = MotionProfile::kGoStraight;
|
||||
request.goal = goal;
|
||||
request.tolerance.xy = xy_goal_tolerance;
|
||||
return submit(request);
|
||||
}
|
||||
|
||||
bool NavigationServer::rotateTo(const robot_geometry_msgs::PoseStamped& goal,
|
||||
double yaw_goal_tolerance)
|
||||
{
|
||||
NavigationRequest request;
|
||||
request.profile = MotionProfile::kRotate;
|
||||
request.goal = goal;
|
||||
request.tolerance.yaw = yaw_goal_tolerance;
|
||||
return submit(request);
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// Điều khiển vòng đời
|
||||
// ================================================================================================
|
||||
|
||||
void NavigationServer::pause()
|
||||
{
|
||||
loop_.requestPause();
|
||||
}
|
||||
|
||||
void NavigationServer::resume()
|
||||
{
|
||||
loop_.requestResume();
|
||||
}
|
||||
|
||||
void NavigationServer::cancel()
|
||||
{
|
||||
loop_.requestCancel();
|
||||
}
|
||||
|
||||
bool NavigationServer::setTwistLinear(const robot_geometry_msgs::Vector3& linear)
|
||||
{
|
||||
// Không phải lệnh jog dù tên nghe như vậy: đây là TRẦN vận tốc, dấu chọn chiều, và host truyền
|
||||
// xuống đây tốc độ đã bị tầng an toàn hạ (amr_control.cpp:561, 671-680).
|
||||
if (!std::isfinite(linear.x) || !std::isfinite(linear.y) || !std::isfinite(linear.z))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Chỉ cất lại. Host gọi từ thread của nó (OPC-UA/VDA5050/ROS) còn ControllerPort không
|
||||
// thread-safe, nên việc đẩy xuống controller thuộc về spinOnce() — xem pushHostInputsToController.
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
if (linear.x < 0.0)
|
||||
{
|
||||
pending_linear_backward_ = linear;
|
||||
has_pending_linear_backward_ = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
pending_linear_forward_ = linear;
|
||||
has_pending_linear_forward_ = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NavigationServer::setTwistAngular(const robot_geometry_msgs::Vector3& angular)
|
||||
{
|
||||
if (!std::isfinite(angular.x) || !std::isfinite(angular.y) || !std::isfinite(angular.z))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
pending_angular_ = angular;
|
||||
has_pending_angular_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void NavigationServer::pushHostInputsToController()
|
||||
{
|
||||
ControllerPort* controller = loop_.controllerPort();
|
||||
if (controller == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
robot_geometry_msgs::Vector3 linear_forward;
|
||||
robot_geometry_msgs::Vector3 linear_backward;
|
||||
robot_geometry_msgs::Vector3 angular;
|
||||
bool push_forward = false;
|
||||
bool push_backward = false;
|
||||
bool push_angular = false;
|
||||
robot_geometry_msgs::Twist velocity;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
|
||||
push_forward = has_pending_linear_forward_;
|
||||
push_backward = has_pending_linear_backward_;
|
||||
push_angular = has_pending_angular_;
|
||||
linear_forward = pending_linear_forward_;
|
||||
linear_backward = pending_linear_backward_;
|
||||
angular = pending_angular_;
|
||||
has_pending_linear_forward_ = false;
|
||||
has_pending_linear_backward_ = false;
|
||||
has_pending_angular_ = false;
|
||||
|
||||
velocity = odometry_.twist.twist;
|
||||
}
|
||||
|
||||
// Đẩy xuống NGOÀI lock: controller là plugin bên thứ ba, thời gian chạy của nó không được phép
|
||||
// chặn các getter mà host đang gọi.
|
||||
if (push_forward)
|
||||
{
|
||||
controller->setTwistLinear(linear_forward);
|
||||
}
|
||||
if (push_backward)
|
||||
{
|
||||
controller->setTwistLinear(linear_backward);
|
||||
}
|
||||
if (push_angular)
|
||||
{
|
||||
controller->setTwistAngular(angular);
|
||||
}
|
||||
|
||||
controller->setMeasuredVelocity(velocity);
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// Đọc trạng thái
|
||||
// ================================================================================================
|
||||
|
||||
bool NavigationServer::getRobotPose(robot_geometry_msgs::PoseStamped& pose)
|
||||
{
|
||||
PosePort* port = loop_.posePort();
|
||||
if (port == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Hỏi thẳng cổng mỗi lần, không cache: mất TF phải nhìn thấy được ngay tại lời gọi này chứ không
|
||||
// phải nhận về một pose cũ đã hết hạn.
|
||||
return port->getRobotPose(pose);
|
||||
}
|
||||
|
||||
bool NavigationServer::getRobotPose(robot_geometry_msgs::Pose2D& pose)
|
||||
{
|
||||
robot_geometry_msgs::PoseStamped stamped;
|
||||
if (!getRobotPose(stamped))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const robot_nav_2d_msgs::Pose2DStamped converted =
|
||||
robot_nav_2d_utils::poseStampedToPose2D(stamped);
|
||||
pose = converted.pose;
|
||||
return true;
|
||||
}
|
||||
|
||||
robot_nav_2d_msgs::Twist2DStamped NavigationServer::getTwist()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(data_mutex_);
|
||||
return twist_;
|
||||
}
|
||||
|
||||
robot::move_base_core::NavFeedback* NavigationServer::getFeedback()
|
||||
{
|
||||
return nav_feedback_.get();
|
||||
}
|
||||
|
||||
robot::move_base_core::PlannerDataOutput NavigationServer::getGlobalData()
|
||||
{
|
||||
return global_data_;
|
||||
}
|
||||
|
||||
robot::move_base_core::PlannerDataOutput NavigationServer::getLocalData()
|
||||
{
|
||||
return local_data_;
|
||||
}
|
||||
|
||||
} // namespace move_base2
|
||||
135
src/navigation_state.cpp
Normal file
135
src/navigation_state.cpp
Normal file
@@ -0,0 +1,135 @@
|
||||
/*********************************************************************
|
||||
*
|
||||
* Software License Agreement (BSD License)
|
||||
*
|
||||
* move_base2 — tên state và phân loại state.
|
||||
*
|
||||
* Author: DuongTD
|
||||
*********************************************************************/
|
||||
#include <move_base2/core/navigation_state.h>
|
||||
|
||||
#include <move_base2/core/navigation_request.h>
|
||||
#include <move_base2/core/velocity_arbiter.h>
|
||||
#include <move_base2/ports/mission_port.h>
|
||||
#include <move_base2/ports/recovery_port.h>
|
||||
|
||||
namespace move_base2
|
||||
{
|
||||
|
||||
const char* toString(NavigationState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case NavigationState::kIdle:
|
||||
return "IDLE";
|
||||
case NavigationState::kPlanning:
|
||||
return "PLANNING";
|
||||
case NavigationState::kControlling:
|
||||
return "CONTROLLING";
|
||||
case NavigationState::kRecovering:
|
||||
return "RECOVERING";
|
||||
case NavigationState::kExecutingActions:
|
||||
return "EXECUTING_ACTIONS";
|
||||
case NavigationState::kPaused:
|
||||
return "PAUSED";
|
||||
case NavigationState::kCancelling:
|
||||
return "CANCELLING";
|
||||
case NavigationState::kSucceeded:
|
||||
return "SUCCEEDED";
|
||||
case NavigationState::kAborted:
|
||||
return "ABORTED";
|
||||
case NavigationState::kCancelled:
|
||||
return "CANCELLED";
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
bool isTerminal(NavigationState state)
|
||||
{
|
||||
return state == NavigationState::kSucceeded || state == NavigationState::kAborted ||
|
||||
state == NavigationState::kCancelled;
|
||||
}
|
||||
|
||||
bool mustBeStopped(NavigationState state)
|
||||
{
|
||||
// Chỉ hai state được phép có vận tốc khác 0: kControlling (local planner lái) và kRecovering
|
||||
// (recovery behavior lái). Mọi state còn lại là hàng rào an toàn — kể cả kExecutingActions:
|
||||
// theo D8 action chạy khi robot đứng yên, action cần chuyển động phải là motion profile.
|
||||
return state != NavigationState::kControlling && state != NavigationState::kRecovering;
|
||||
}
|
||||
|
||||
const char* toString(MotionProfile profile)
|
||||
{
|
||||
switch (profile)
|
||||
{
|
||||
case MotionProfile::kPosition:
|
||||
return "position";
|
||||
case MotionProfile::kDocking:
|
||||
return "docking";
|
||||
case MotionProfile::kGoStraight:
|
||||
return "go_straight";
|
||||
case MotionProfile::kRotate:
|
||||
return "rotate";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const char* toString(NavigationOutcome outcome)
|
||||
{
|
||||
switch (outcome)
|
||||
{
|
||||
case NavigationOutcome::kSucceeded:
|
||||
return "SUCCEEDED";
|
||||
case NavigationOutcome::kFailed:
|
||||
return "FAILED";
|
||||
case NavigationOutcome::kCancelled:
|
||||
return "CANCELLED";
|
||||
case NavigationOutcome::kPreempted:
|
||||
return "PREEMPTED";
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
const char* toString(RecoveryTrigger trigger)
|
||||
{
|
||||
switch (trigger)
|
||||
{
|
||||
case RecoveryTrigger::kPlanningFailed:
|
||||
return "planning_failed";
|
||||
case RecoveryTrigger::kControllingFailed:
|
||||
return "controlling_failed";
|
||||
case RecoveryTrigger::kOscillation:
|
||||
return "oscillation";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const char* toString(RecoveryOutputKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case RecoveryOutputKind::kNone:
|
||||
return "none";
|
||||
case RecoveryOutputKind::kVelocity:
|
||||
return "velocity";
|
||||
case RecoveryOutputKind::kPath:
|
||||
return "path";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const char* toString(VelocitySource source)
|
||||
{
|
||||
switch (source)
|
||||
{
|
||||
case VelocitySource::kNone:
|
||||
return "none";
|
||||
case VelocitySource::kController:
|
||||
return "controller";
|
||||
case VelocitySource::kRecovery:
|
||||
return "recovery";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
} // namespace move_base2
|
||||
302
src/runners/action_runner.cpp
Normal file
302
src/runners/action_runner.cpp
Normal file
@@ -0,0 +1,302 @@
|
||||
/*********************************************************************
|
||||
* move_base2 — hiện thực ActionPort bằng các ActionHandler plugin.
|
||||
*
|
||||
* Author: DuongTD
|
||||
*********************************************************************/
|
||||
#include <move_base2/runners/action_runner.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <boost/dll/import.hpp>
|
||||
#include <boost/system/system_error.hpp>
|
||||
#include <yaml-cpp/yaml.h>
|
||||
|
||||
#include <robot/robot.h>
|
||||
|
||||
namespace move_base2
|
||||
{
|
||||
|
||||
ActionRunner::~ActionRunner()
|
||||
{
|
||||
// Handler phải chết TRƯỚC factory: factory là thứ giữ .so còn nạp.
|
||||
active_ = nullptr;
|
||||
by_type_.clear();
|
||||
handlers_.clear();
|
||||
factories_.clear();
|
||||
}
|
||||
|
||||
void ActionRunner::setClock(ClockPort* clock)
|
||||
{
|
||||
clock_ = clock;
|
||||
}
|
||||
|
||||
void ActionRunner::setNamespace(const std::string& ns)
|
||||
{
|
||||
namespace_ = ns;
|
||||
}
|
||||
|
||||
bool ActionRunner::registerHandler(const ActionHandler::Ptr& handler)
|
||||
{
|
||||
if (!handler)
|
||||
{
|
||||
robot::log_error("[move_base2] ActionRunner: handler null.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::vector<std::string> types = handler->supportedActionTypes();
|
||||
if (types.empty())
|
||||
{
|
||||
robot::log_error("[move_base2] ActionRunner: handler không khai actionType nào — sẽ không bao "
|
||||
"giờ được gọi.");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const std::string& type : types)
|
||||
{
|
||||
if (type.empty())
|
||||
{
|
||||
robot::log_error("[move_base2] ActionRunner: handler khai một actionType rỗng.");
|
||||
return false;
|
||||
}
|
||||
if (by_type_.find(type) != by_type_.end())
|
||||
{
|
||||
// Hai handler cùng nhận một type thì việc định tuyến phụ thuộc thứ tự nạp — từ chối thay vì
|
||||
// im lặng ghi đè.
|
||||
robot::log_error("[move_base2] ActionRunner: actionType '%s' đã có handler khác đăng ký.",
|
||||
type.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
handlers_.push_back(handler);
|
||||
for (const std::string& type : types)
|
||||
{
|
||||
by_type_[type] = handler.get();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ActionHandler* ActionRunner::find(const std::string& action_type) const
|
||||
{
|
||||
const auto it = by_type_.find(action_type);
|
||||
return it == by_type_.end() ? nullptr : it->second;
|
||||
}
|
||||
|
||||
std::vector<std::string> ActionRunner::supportedActionTypes() const
|
||||
{
|
||||
std::vector<std::string> types;
|
||||
types.reserve(by_type_.size());
|
||||
for (const auto& entry : by_type_)
|
||||
{
|
||||
types.push_back(entry.first);
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
bool ActionRunner::loadOne(const std::string& name, const std::string& type,
|
||||
robot::NodeHandle& nh, const std::string& ns)
|
||||
{
|
||||
robot::PluginLoaderHelper loader(nh);
|
||||
const std::string library_path = loader.findLibraryPath(type);
|
||||
|
||||
if (library_path.empty())
|
||||
{
|
||||
robot::log_error("[move_base2] ActionRunner: không tìm được thư viện cho '%s' — kiểm khoá "
|
||||
"'%s/library_path' trong YAML và sự tồn tại của file .so.",
|
||||
type.c_str(), type.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::function<ActionHandler::Ptr()> factory;
|
||||
try
|
||||
{
|
||||
factory = boost::dll::import_alias<ActionHandler::Ptr()>(
|
||||
library_path, type, boost::dll::load_mode::append_decorations);
|
||||
}
|
||||
catch (const boost::system::system_error& ex)
|
||||
{
|
||||
robot::log_error("[move_base2] ActionRunner: không nạp được symbol '%s' từ '%s': %s",
|
||||
type.c_str(), library_path.c_str(), ex.what());
|
||||
return false;
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
robot::log_error("[move_base2] ActionRunner: lỗi khi nạp '%s': %s", type.c_str(), ex.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
ActionHandler::Ptr handler;
|
||||
try
|
||||
{
|
||||
handler = factory();
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
robot::log_error("[move_base2] ActionRunner: factory của '%s' ném exception: %s", type.c_str(),
|
||||
ex.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!handler)
|
||||
{
|
||||
robot::log_error("[move_base2] ActionRunner: factory của '%s' trả về null.", type.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string param_ns = ns.empty() ? name : ns + "/" + name;
|
||||
robot::NodeHandle handler_nh(nh, param_ns);
|
||||
|
||||
if (!handler->configure(name, handler_nh))
|
||||
{
|
||||
robot::log_error("[move_base2] ActionRunner: '%s' (instance '%s') configure() thất bại.",
|
||||
type.c_str(), name.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!registerHandler(handler))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
factories_.push_back(std::move(factory));
|
||||
|
||||
robot::log_info("[move_base2] ActionRunner: nạp '%s' (instance '%s').", type.c_str(),
|
||||
name.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ActionRunner::configure(robot::NodeHandle& nh)
|
||||
{
|
||||
if (configured_)
|
||||
{
|
||||
robot::log_error("[move_base2] ActionRunner: configure() gọi lần thứ hai.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (clock_ == nullptr)
|
||||
{
|
||||
robot::log_error("[move_base2] ActionRunner: thiếu ClockPort — handler không có mốc timeout.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string key = namespace_.empty() ? std::string("handlers") : namespace_ + "/handlers";
|
||||
|
||||
YAML::Node list;
|
||||
if (!nh.getParam(key, list) || !list.IsSequence())
|
||||
{
|
||||
// Không có handler nào là hợp lệ: hệ không có thiết bị thì mọi mission đều nav-only, và
|
||||
// ControlLoop::submit đã từ chối yêu cầu mang action ngay tại cửa.
|
||||
robot::log_warning("[move_base2] ActionRunner: '%s' không có danh sách handler — runtime sẽ "
|
||||
"từ chối mọi yêu cầu mang action.", key.c_str());
|
||||
configured_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool all_ok = true;
|
||||
|
||||
for (std::size_t i = 0; i < list.size(); ++i)
|
||||
{
|
||||
const YAML::Node& entry = list[i];
|
||||
|
||||
if (!entry.IsMap() || !entry["type"])
|
||||
{
|
||||
robot::log_error("[move_base2] ActionRunner: '%s[%zu]' thiếu khoá 'type'.", key.c_str(), i);
|
||||
all_ok = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string type;
|
||||
std::string name;
|
||||
try
|
||||
{
|
||||
type = entry["type"].as<std::string>();
|
||||
name = entry["name"] ? entry["name"].as<std::string>() : type;
|
||||
}
|
||||
catch (const YAML::Exception& ex)
|
||||
{
|
||||
robot::log_error("[move_base2] ActionRunner: '%s[%zu]' không đọc được: %s", key.c_str(), i,
|
||||
ex.what());
|
||||
all_ok = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!loadOne(name, type, nh, namespace_))
|
||||
{
|
||||
all_ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
configured_ = true;
|
||||
return all_ok;
|
||||
}
|
||||
|
||||
bool ActionRunner::start(const robot_protocol_msgs::Action& action)
|
||||
{
|
||||
active_ = nullptr;
|
||||
active_action_id_.clear();
|
||||
|
||||
if (!configured_)
|
||||
{
|
||||
robot::log_error("[move_base2] ActionRunner: start() trước configure().");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (action.actionType.empty())
|
||||
{
|
||||
robot::log_error("[move_base2] ActionRunner: action không có actionType.");
|
||||
return false;
|
||||
}
|
||||
|
||||
ActionHandler* handler = find(action.actionType);
|
||||
if (handler == nullptr)
|
||||
{
|
||||
robot::log_error("[move_base2] ActionRunner: không handler nào nhận actionType '%s' (id '%s').",
|
||||
action.actionType.c_str(), action.actionId.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!handler->start(action, clock_->now()))
|
||||
{
|
||||
robot::log_warning("[move_base2] ActionRunner: handler từ chối khởi động action '%s' (id '%s').",
|
||||
action.actionType.c_str(), action.actionId.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
active_ = handler;
|
||||
active_action_id_ = action.actionId;
|
||||
return true;
|
||||
}
|
||||
|
||||
ActionTick ActionRunner::update()
|
||||
{
|
||||
ActionTick tick;
|
||||
|
||||
if (active_ == nullptr)
|
||||
{
|
||||
// Contract nói update() chỉ được gọi sau start() trả true. Vẫn guard: lỗi thứ tự gọi phải thành
|
||||
// "action này hỏng" chứ không phải dereference null.
|
||||
tick.status = ActionTick::Status::kFailed;
|
||||
tick.message = "update() khi không có action nào đang chạy";
|
||||
return tick;
|
||||
}
|
||||
|
||||
tick = active_->update(clock_->now());
|
||||
|
||||
if (tick.status != ActionTick::Status::kRunning)
|
||||
{
|
||||
active_ = nullptr;
|
||||
}
|
||||
|
||||
return tick;
|
||||
}
|
||||
|
||||
void ActionRunner::cancel()
|
||||
{
|
||||
if (active_ != nullptr)
|
||||
{
|
||||
active_->cancel();
|
||||
active_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace move_base2
|
||||
408
src/runners/controller_runner.cpp
Normal file
408
src/runners/controller_runner.cpp
Normal file
@@ -0,0 +1,408 @@
|
||||
/*********************************************************************
|
||||
*
|
||||
* Software License Agreement (BSD License)
|
||||
*
|
||||
* move_base2 — cài đặt ControllerRunner.
|
||||
*
|
||||
* Author: DuongTD
|
||||
*********************************************************************/
|
||||
#include <move_base2/runners/controller_runner.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <exception>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/dll/import.hpp>
|
||||
#include <boost/system/system_error.hpp>
|
||||
|
||||
#include <robot/plugin_loader_helper.h>
|
||||
#include <robot/robot.h>
|
||||
|
||||
namespace move_base2
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
/// [s] Giãn cách log cho cảnh báo phát sinh trong đường nóng — nhịp control loop, không được spam.
|
||||
constexpr double kHotPathLogThrottle = 5.0;
|
||||
|
||||
bool isFiniteTwist(const robot_geometry_msgs::Twist& twist)
|
||||
{
|
||||
return std::isfinite(twist.linear.x) && std::isfinite(twist.linear.y) &&
|
||||
std::isfinite(twist.angular.z);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ControllerRunner::ControllerRunner() = default;
|
||||
ControllerRunner::~ControllerRunner() = default;
|
||||
|
||||
bool ControllerRunner::configure(const robot::NodeHandle& nh, tf3::BufferCore* tf,
|
||||
robot_costmap_2d::Costmap2DROBOT* costmap,
|
||||
const std::string& initial_controller, std::string& error)
|
||||
{
|
||||
if (configured_)
|
||||
{
|
||||
error = "ControllerRunner::configure() gọi lần thứ hai";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (costmap == nullptr)
|
||||
{
|
||||
error = "ControllerRunner cần costmap local khác null";
|
||||
return false;
|
||||
}
|
||||
|
||||
nh_ = nh;
|
||||
tf_ = tf;
|
||||
costmap_ = costmap;
|
||||
configured_ = true;
|
||||
|
||||
if (!initial_controller.empty() && !swapPlanner(initial_controller))
|
||||
{
|
||||
error = "không nạp được local planner khởi đầu '" + initial_controller + "'";
|
||||
configured_ = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
robot_nav_core::BaseLocalPlanner* ControllerRunner::acquire(const std::string& name)
|
||||
{
|
||||
const auto cached = controllers_.find(name);
|
||||
if (cached != controllers_.end())
|
||||
{
|
||||
return cached->second.instance.get();
|
||||
}
|
||||
|
||||
robot::PluginLoaderHelper loader(nh_);
|
||||
const std::string library_path = loader.findLibraryPath(name);
|
||||
|
||||
if (library_path.empty())
|
||||
{
|
||||
robot::log_error("[move_base2] ControllerRunner: không tìm được thư viện cho '%s' — kiểm khoá "
|
||||
"'%s/library_path' trong YAML và sự tồn tại của file .so trong devel/lib.\n",
|
||||
name.c_str(), name.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Loaded loaded;
|
||||
|
||||
try
|
||||
{
|
||||
loaded.factory = boost::dll::import_alias<robot_nav_core::BaseLocalPlanner::Ptr()>(
|
||||
library_path, name, boost::dll::load_mode::append_decorations);
|
||||
}
|
||||
catch (const boost::system::system_error& ex)
|
||||
{
|
||||
robot::log_error("[move_base2] ControllerRunner: không nạp được symbol '%s' từ '%s': %s\n",
|
||||
name.c_str(), library_path.c_str(), ex.what());
|
||||
return nullptr;
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
robot::log_error("[move_base2] ControllerRunner: lỗi khi nạp '%s': %s\n", name.c_str(),
|
||||
ex.what());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
loaded.instance = loaded.factory();
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
robot::log_error("[move_base2] ControllerRunner: factory của '%s' ném exception: %s\n",
|
||||
name.c_str(), ex.what());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!loaded.instance)
|
||||
{
|
||||
robot::log_error("[move_base2] ControllerRunner: factory của '%s' trả nullptr.\n", name.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Khác BaseGlobalPlanner: initialize ở đây trả void, nên không có cách nào biết plugin tự thấy
|
||||
// mình hỏng. Chỉ chặn được exception.
|
||||
loaded.instance->initialize(name, tf_, costmap_);
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
robot::log_error("[move_base2] ControllerRunner: initialize() của '%s' ném exception: %s\n",
|
||||
name.c_str(), ex.what());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto inserted = controllers_.emplace(name, std::move(loaded));
|
||||
return inserted.first->second.instance.get();
|
||||
}
|
||||
|
||||
void ControllerRunner::applyPendingLimits(robot_nav_core::BaseLocalPlanner* controller)
|
||||
{
|
||||
if (controller == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Trần vận tốc thuộc về YÊU CẦU, không thuộc về instance planner. Instance mới nạp không biết gì
|
||||
// về các trần host đã đặt trước đó — không áp lại là robot lặng lẽ chạy nhanh hơn mức tầng an
|
||||
// toàn cho phép, và không có dấu hiệu nào cả.
|
||||
try
|
||||
{
|
||||
if (has_limit_linear_forward_)
|
||||
{
|
||||
controller->setTwistLinear(limit_linear_forward_);
|
||||
}
|
||||
if (has_limit_linear_backward_)
|
||||
{
|
||||
controller->setTwistLinear(limit_linear_backward_);
|
||||
}
|
||||
if (has_limit_angular_)
|
||||
{
|
||||
controller->setTwistAngular(limit_angular_);
|
||||
}
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
robot::log_error("[move_base2] ControllerRunner: lỗi khi áp lại trần vận tốc: %s\n", ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
bool ControllerRunner::swapPlanner(const std::string& planner_name)
|
||||
{
|
||||
if (!configured_)
|
||||
{
|
||||
robot::log_error("[move_base2] ControllerRunner: swapPlanner() trước configure().\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (planner_name.empty())
|
||||
{
|
||||
robot::log_error("[move_base2] ControllerRunner: tên controller rỗng.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (planner_name == active_name_ && active_ != nullptr)
|
||||
{
|
||||
return true; // Đã đúng controller; không log để khỏi spam ở cửa vào mỗi yêu cầu.
|
||||
}
|
||||
|
||||
robot_nav_core::BaseLocalPlanner* controller = acquire(planner_name);
|
||||
if (controller == nullptr)
|
||||
{
|
||||
// Giữ nguyên controller đang chạy: bên gọi từ chối yêu cầu dựa vào giá trị trả về.
|
||||
return false;
|
||||
}
|
||||
|
||||
active_ = controller;
|
||||
active_name_ = planner_name;
|
||||
applyPendingLimits(active_);
|
||||
|
||||
robot::log_info("[move_base2] ControllerRunner: local planner đang dùng là '%s'.\n",
|
||||
planner_name.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
void ControllerRunner::setTolerance(double xy_m, double yaw_rad)
|
||||
{
|
||||
if (!configured_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Interface gen-1 không có hàm đặt sai số; bản cũ ghi vào param rồi để planner tự đọc lại. Kênh
|
||||
// gián tiếp này được giữ nguyên để không đổi hành vi của các planner đang chạy — nhưng planner
|
||||
// nào chỉ đọc param lúc initialize sẽ KHÔNG thấy giá trị mới. Xem doc của lớp.
|
||||
nh_.setParam("xy_goal_tolerance", xy_m);
|
||||
nh_.setParam("yaw_goal_tolerance", yaw_rad);
|
||||
}
|
||||
|
||||
bool ControllerRunner::setPlan(const std::vector<robot_geometry_msgs::PoseStamped>& plan)
|
||||
{
|
||||
if (!configured_ || active_ == nullptr)
|
||||
{
|
||||
robot::log_error_throttle(kHotPathLogThrottle,
|
||||
"[move_base2] ControllerRunner: setPlan() khi chưa có controller.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (plan.empty())
|
||||
{
|
||||
// Plan rỗng lọt xuống sẽ thành front()/back() trên vector rỗng bên trong planner.
|
||||
robot::log_error_throttle(kHotPathLogThrottle,
|
||||
"[move_base2] ControllerRunner: từ chối plan rỗng.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return active_->setPlan(plan);
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
robot::log_error_throttle(kHotPathLogThrottle,
|
||||
"[move_base2] ControllerRunner: '%s' ném exception trong setPlan: "
|
||||
"%s\n", active_name_.c_str(), ex.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ControllerRunner::computeVelocityCommands(robot_geometry_msgs::Twist& cmd)
|
||||
{
|
||||
cmd = robot_geometry_msgs::Twist();
|
||||
|
||||
if (!configured_ || active_ == nullptr)
|
||||
{
|
||||
robot::log_error_throttle(kHotPathLogThrottle,
|
||||
"[move_base2] ControllerRunner: computeVelocityCommands() khi chưa "
|
||||
"có controller.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
robot_geometry_msgs::Twist result;
|
||||
bool ok = false;
|
||||
|
||||
try
|
||||
{
|
||||
ok = active_->computeVelocityCommands(measured_velocity_, result);
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
robot::log_error_throttle(kHotPathLogThrottle,
|
||||
"[move_base2] ControllerRunner: '%s' ném exception khi tính lệnh: "
|
||||
"%s\n", active_name_.c_str(), ex.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isFiniteTwist(result))
|
||||
{
|
||||
// VelocityArbiter cũng chặn NaN/Inf, nhưng chặn ngay tại nguồn cho biết ĐÚNG plugin nào đang
|
||||
// trả dữ liệu hỏng — arbiter chỉ thấy một con số vô nghĩa không rõ từ đâu.
|
||||
robot::log_error_throttle(kHotPathLogThrottle,
|
||||
"[move_base2] ControllerRunner: '%s' trả lệnh chứa NaN/Inf.\n",
|
||||
active_name_.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
cmd = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ControllerRunner::isGoalReached()
|
||||
{
|
||||
if (!configured_ || active_ == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return active_->isGoalReached();
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
// Trả false: "chưa tới đích" là phía an toàn — báo nhầm đã tới sẽ kết thúc chặng đường sớm và
|
||||
// robot dừng ở chỗ không phải đích.
|
||||
robot::log_error_throttle(kHotPathLogThrottle,
|
||||
"[move_base2] ControllerRunner: '%s' ném exception trong "
|
||||
"isGoalReached: %s\n", active_name_.c_str(), ex.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void ControllerRunner::setMeasuredVelocity(const robot_geometry_msgs::Twist& velocity)
|
||||
{
|
||||
if (!isFiniteTwist(velocity))
|
||||
{
|
||||
// Giữ giá trị cũ thay vì đưa NaN vào hàm tính lệnh — nhiều local planner dùng nó làm mốc giới
|
||||
// hạn gia tốc, và NaN ở đó lan ra toàn bộ cost function.
|
||||
robot::log_error_throttle(kHotPathLogThrottle,
|
||||
"[move_base2] ControllerRunner: bỏ vận tốc đo được chứa NaN/Inf.\n");
|
||||
return;
|
||||
}
|
||||
measured_velocity_ = velocity;
|
||||
}
|
||||
|
||||
bool ControllerRunner::setTwistLinear(const robot_geometry_msgs::Vector3& linear)
|
||||
{
|
||||
if (!std::isfinite(linear.x) || !std::isfinite(linear.y) || !std::isfinite(linear.z))
|
||||
{
|
||||
robot::log_error("[move_base2] ControllerRunner: trần vận tốc thẳng chứa NaN/Inf, bỏ qua.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Dấu chọn chiều — xem doc của ControllerPort::setTwistLinear. Nhớ cả hai chiều riêng để
|
||||
// swapPlanner còn áp lại được lên instance mới.
|
||||
if (linear.x < 0.0)
|
||||
{
|
||||
limit_linear_backward_ = linear;
|
||||
has_limit_linear_backward_ = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
limit_linear_forward_ = linear;
|
||||
has_limit_linear_forward_ = true;
|
||||
}
|
||||
|
||||
if (active_ == nullptr)
|
||||
{
|
||||
// Host đặt trần trước khi controller được nạp là chuyện bình thường: thứ tự khởi tạo không do
|
||||
// move_base2 quyết. Đã nhớ lại, sẽ áp khi có controller.
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return active_->setTwistLinear(linear);
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
robot::log_error("[move_base2] ControllerRunner: '%s' ném exception trong setTwistLinear: %s\n",
|
||||
active_name_.c_str(), ex.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ControllerRunner::setTwistAngular(const robot_geometry_msgs::Vector3& angular)
|
||||
{
|
||||
if (!std::isfinite(angular.x) || !std::isfinite(angular.y) || !std::isfinite(angular.z))
|
||||
{
|
||||
robot::log_error("[move_base2] ControllerRunner: trần vận tốc góc chứa NaN/Inf, bỏ qua.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
limit_angular_ = angular;
|
||||
has_limit_angular_ = true;
|
||||
|
||||
if (active_ == nullptr)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return active_->setTwistAngular(angular);
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
robot::log_error("[move_base2] ControllerRunner: '%s' ném exception trong setTwistAngular: %s\n",
|
||||
active_name_.c_str(), ex.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::string ControllerRunner::activeController() const
|
||||
{
|
||||
return active_ != nullptr ? active_name_ : std::string();
|
||||
}
|
||||
|
||||
} // namespace move_base2
|
||||
395
src/runners/planner_runner.cpp
Normal file
395
src/runners/planner_runner.cpp
Normal file
@@ -0,0 +1,395 @@
|
||||
/*********************************************************************
|
||||
*
|
||||
* Software License Agreement (BSD License)
|
||||
*
|
||||
* move_base2 — cài đặt PlannerRunner.
|
||||
*
|
||||
* Author: DuongTD
|
||||
*********************************************************************/
|
||||
#include <move_base2/runners/planner_runner.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <exception>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/dll/import.hpp>
|
||||
#include <boost/system/system_error.hpp>
|
||||
|
||||
#include <robot/plugin_loader_helper.h>
|
||||
#include <robot/robot.h>
|
||||
|
||||
namespace move_base2
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
/// @brief Pose có dùng được để lập plan không — NaN/Inf lọt vào SBPL là hỏng ở tầng khó truy nhất.
|
||||
bool isFinitePose(const robot_geometry_msgs::PoseStamped& pose)
|
||||
{
|
||||
return std::isfinite(pose.pose.position.x) && std::isfinite(pose.pose.position.y) &&
|
||||
std::isfinite(pose.pose.orientation.z) && std::isfinite(pose.pose.orientation.w);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PlannerRunner::PlannerRunner() = default;
|
||||
|
||||
PlannerRunner::~PlannerRunner()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
shutdown_ = true;
|
||||
discard_ = true;
|
||||
}
|
||||
cv_.notify_all();
|
||||
|
||||
if (thread_.joinable())
|
||||
{
|
||||
// Chờ có chủ đích: plugin là hộp đen nạp lúc chạy, không có đường cắt ngang một phép tính đang
|
||||
// chạy. Detach thay vì join sẽ để thread chạm vào `planning_`/`handoff_` sau khi chúng đã bị
|
||||
// huỷ — hỏng ở chỗ không thể truy được.
|
||||
thread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
bool PlannerRunner::configure(const robot::NodeHandle& nh,
|
||||
robot_costmap_2d::Costmap2DROBOT* costmap,
|
||||
const std::string& initial_planner, std::string& error)
|
||||
{
|
||||
if (configured_)
|
||||
{
|
||||
error = "PlannerRunner::configure() gọi lần thứ hai";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (costmap == nullptr)
|
||||
{
|
||||
// Không có costmap thì `BaseGlobalPlanner::initialize` nhận nullptr và mọi plugin tự quyết định
|
||||
// làm gì với nó — thường là sập. Chặn ở đây, nơi còn nói được lý do.
|
||||
error = "PlannerRunner cần costmap global khác null";
|
||||
return false;
|
||||
}
|
||||
|
||||
nh_ = nh;
|
||||
costmap_ = costmap;
|
||||
configured_ = true;
|
||||
|
||||
if (!initial_planner.empty() && !swapPlanner(initial_planner))
|
||||
{
|
||||
error = "không nạp được global planner khởi đầu '" + initial_planner + "'";
|
||||
configured_ = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
thread_ = std::thread(&PlannerRunner::threadBody, this);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// Nạp plugin — chỉ control thread chạm
|
||||
// ================================================================================================
|
||||
|
||||
robot_nav_core::BaseGlobalPlanner* PlannerRunner::acquire(const std::string& name)
|
||||
{
|
||||
const auto cached = planners_.find(name);
|
||||
if (cached != planners_.end())
|
||||
{
|
||||
return cached->second.instance.get();
|
||||
}
|
||||
|
||||
robot::PluginLoaderHelper loader(nh_);
|
||||
const std::string library_path = loader.findLibraryPath(name);
|
||||
|
||||
if (library_path.empty())
|
||||
{
|
||||
robot::log_error("[move_base2] PlannerRunner: không tìm được thư viện cho '%s' — kiểm khoá "
|
||||
"'%s/library_path' trong YAML và sự tồn tại của file .so trong devel/lib.\n",
|
||||
name.c_str(), name.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Loaded loaded;
|
||||
|
||||
try
|
||||
{
|
||||
loaded.factory = boost::dll::import_alias<robot_nav_core::BaseGlobalPlanner::Ptr()>(
|
||||
library_path, name, boost::dll::load_mode::append_decorations);
|
||||
}
|
||||
catch (const boost::system::system_error& ex)
|
||||
{
|
||||
robot::log_error("[move_base2] PlannerRunner: không nạp được symbol '%s' từ '%s': %s\n",
|
||||
name.c_str(), library_path.c_str(), ex.what());
|
||||
return nullptr;
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
robot::log_error("[move_base2] PlannerRunner: lỗi khi nạp '%s': %s\n", name.c_str(), ex.what());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
loaded.instance = loaded.factory();
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
robot::log_error("[move_base2] PlannerRunner: factory của '%s' ném exception: %s\n",
|
||||
name.c_str(), ex.what());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!loaded.instance)
|
||||
{
|
||||
robot::log_error("[move_base2] PlannerRunner: factory của '%s' trả nullptr.\n", name.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool initialized = false;
|
||||
try
|
||||
{
|
||||
initialized = loaded.instance->initialize(name, costmap_);
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
robot::log_error("[move_base2] PlannerRunner: initialize() của '%s' ném exception: %s\n",
|
||||
name.c_str(), ex.what());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!initialized)
|
||||
{
|
||||
// Bản cũ chỉ log rồi đi tiếp với một planner chưa khởi tạo. Ở đây coi là thất bại: một planner
|
||||
// báo "tôi chưa sẵn sàng" mà vẫn được gọi makePlan là đường dẫn tới hành vi không xác định.
|
||||
robot::log_error("[move_base2] PlannerRunner: '%s' báo initialize() thất bại.\n", name.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Chỉ đưa vào cache khi đã khởi tạo xong — cache một instance hỏng nghĩa là mọi lần thử lại sau
|
||||
// đều nhận lại đúng cái hỏng đó mà không báo gì.
|
||||
const auto inserted = planners_.emplace(name, std::move(loaded));
|
||||
return inserted.first->second.instance.get();
|
||||
}
|
||||
|
||||
bool PlannerRunner::swapPlanner(const std::string& planner_name)
|
||||
{
|
||||
if (!configured_)
|
||||
{
|
||||
robot::log_error("[move_base2] PlannerRunner: swapPlanner() trước configure().\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (planner_name.empty())
|
||||
{
|
||||
robot::log_error("[move_base2] PlannerRunner: tên planner rỗng.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (planner_name == active_name_)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (active_ != nullptr)
|
||||
{
|
||||
return true; // Đã đúng planner; không log để khỏi spam ở cửa vào mỗi yêu cầu.
|
||||
}
|
||||
}
|
||||
|
||||
robot_nav_core::BaseGlobalPlanner* planner = acquire(planner_name);
|
||||
if (planner == nullptr)
|
||||
{
|
||||
// Giữ nguyên planner đang chạy. Bên gọi từ chối yêu cầu dựa vào giá trị trả về; chuyển sang
|
||||
// trạng thái "không có planner" ở đây sẽ làm hỏng luôn cả yêu cầu đang chạy dở.
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
// Lượt đang chạy thuộc về planner cũ — kết quả của nó không còn nghĩa. Không cắt ngang được
|
||||
// phép tính, chỉ đánh dấu vứt kết quả. Con trỏ planner cũ vẫn hợp lệ vì cache không xoá entry.
|
||||
if (running_ || pending_)
|
||||
{
|
||||
discard_ = true;
|
||||
}
|
||||
active_ = planner;
|
||||
}
|
||||
|
||||
active_name_ = planner_name;
|
||||
robot::log_info("[move_base2] PlannerRunner: global planner đang dùng là '%s'.\n",
|
||||
planner_name.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string PlannerRunner::activePlanner() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return active_ != nullptr ? active_name_ : std::string();
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// Vòng đời một lượt lập plan
|
||||
// ================================================================================================
|
||||
|
||||
bool PlannerRunner::startPlan(const robot_geometry_msgs::PoseStamped& start,
|
||||
const robot_geometry_msgs::PoseStamped& goal,
|
||||
const robot_protocol_msgs::Order* order, std::uint64_t tag)
|
||||
{
|
||||
if (!configured_)
|
||||
{
|
||||
robot::log_error_throttle(5.0, "[move_base2] PlannerRunner: startPlan() trước configure().\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isFinitePose(start) || !isFinitePose(goal))
|
||||
{
|
||||
robot::log_error_throttle(5.0, "[move_base2] PlannerRunner: start hoặc goal chứa NaN/Inf, "
|
||||
"không khởi động lượt lập plan.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
if (active_ == nullptr)
|
||||
{
|
||||
robot::log_error_throttle(5.0, "[move_base2] PlannerRunner: startPlan() khi chưa có planner.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pending_ || running_)
|
||||
{
|
||||
return false; // Đã có lượt đang chạy; bên gọi phải chờ hoặc huỷ trước.
|
||||
}
|
||||
|
||||
request_start_ = start;
|
||||
request_goal_ = goal;
|
||||
// Sao chép Order: con trỏ chỉ hợp lệ trong lời gọi này, còn lượt lập plan sống lâu hơn thế.
|
||||
request_order_ = (order != nullptr) ? std::make_shared<robot_protocol_msgs::Order>(*order)
|
||||
: nullptr;
|
||||
request_tag_ = tag;
|
||||
|
||||
pending_ = true;
|
||||
discard_ = false;
|
||||
has_result_ = false;
|
||||
|
||||
cv_.notify_one();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlannerRunner::isPlanning() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return pending_ || running_;
|
||||
}
|
||||
|
||||
bool PlannerRunner::pollPlan(PlanResult& result)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
if (!has_result_)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
has_result_ = false;
|
||||
result.tag = result_tag_;
|
||||
result.succeeded = result_ok_;
|
||||
|
||||
// Hoán vị chứ không copy: vector cũ của bên gọi quay lại làm hộp thư và giữ nguyên capacity, nên
|
||||
// ở trạng thái ổn định không có lần cấp phát nào cho việc bàn giao plan.
|
||||
result.plan.swap(handoff_);
|
||||
handoff_.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlannerRunner::cancelPlan()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (pending_ || running_)
|
||||
{
|
||||
discard_ = true;
|
||||
}
|
||||
// Kết quả đã nằm sẵn trong hộp thư cũng bỏ luôn: bên gọi vừa nói nó không còn cần plan này.
|
||||
has_result_ = false;
|
||||
handoff_.clear();
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// Thread planner
|
||||
// ================================================================================================
|
||||
|
||||
void PlannerRunner::threadBody()
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
|
||||
while (true)
|
||||
{
|
||||
cv_.wait(lock, [this] { return shutdown_ || pending_; });
|
||||
|
||||
if (shutdown_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Chụp lại yêu cầu rồi NHẢ MUTEX trước khi gọi plugin. Đây là điểm mấu chốt của cả mô hình:
|
||||
// control thread không bao giờ phải chờ một lượt lập plan.
|
||||
robot_nav_core::BaseGlobalPlanner* planner = active_;
|
||||
const robot_geometry_msgs::PoseStamped start = request_start_;
|
||||
const robot_geometry_msgs::PoseStamped goal = request_goal_;
|
||||
const std::shared_ptr<robot_protocol_msgs::Order> order = request_order_;
|
||||
const std::uint64_t tag = request_tag_;
|
||||
|
||||
pending_ = false;
|
||||
running_ = true;
|
||||
|
||||
lock.unlock();
|
||||
|
||||
planning_.clear();
|
||||
bool ok = false;
|
||||
|
||||
try
|
||||
{
|
||||
// Hai overload của interface gốc gộp lại: "có Order hay không" là một nhánh, không phải hai
|
||||
// contract. Plugin nào không hiểu Order thì overload mặc định của nó tự lo.
|
||||
ok = (order != nullptr) ? planner->makePlan(*order, start, goal, planning_)
|
||||
: planner->makePlan(start, goal, planning_);
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
// Plugin bên thứ ba ném ra thì đây là biên duy nhất chặn được — exception thoát khỏi thân
|
||||
// thread là std::terminate, tức mất cả tiến trình navigation vì một lượt lập plan hỏng.
|
||||
robot::log_error_throttle(5.0, "[move_base2] PlannerRunner: plugin ném exception khi lập "
|
||||
"plan: %s\n", ex.what());
|
||||
ok = false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
robot::log_error_throttle(5.0, "[move_base2] PlannerRunner: plugin ném exception lạ khi lập "
|
||||
"plan.\n");
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (!ok || planning_.empty())
|
||||
{
|
||||
// Contract của PlannerPort: thành công nghĩa là plan KHÔNG rỗng. Một số plugin trả true kèm
|
||||
// vector rỗng; quy về thất bại ngay tại đây để tầng trên không gọi front()/back() trên nó.
|
||||
planning_.clear();
|
||||
ok = false;
|
||||
}
|
||||
|
||||
lock.lock();
|
||||
running_ = false;
|
||||
|
||||
if (discard_)
|
||||
{
|
||||
// Lượt này đã bị huỷ hoặc planner đã bị đổi giữa chừng. Vứt lặng lẽ — không phải lỗi.
|
||||
discard_ = false;
|
||||
planning_.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
planning_.swap(handoff_);
|
||||
result_tag_ = tag;
|
||||
result_ok_ = ok;
|
||||
has_result_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace move_base2
|
||||
253
src/runners/recovery_runner.cpp
Normal file
253
src/runners/recovery_runner.cpp
Normal file
@@ -0,0 +1,253 @@
|
||||
/*********************************************************************
|
||||
* move_base2 — hiện thực RecoveryPort bằng recovery_core.
|
||||
*
|
||||
* Author: DuongTD
|
||||
*********************************************************************/
|
||||
#include <move_base2/runners/recovery_runner.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <robot/robot.h>
|
||||
|
||||
namespace move_base2
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
/// Dịch lý do vào recovery sang enum của recovery_core. `switch` đầy đủ để compiler bắt được ngay
|
||||
/// khi một bên thêm giá trị mới — đó là toàn bộ lý do không dùng cast số.
|
||||
recovery_core::RecoveryTrigger toCoreTrigger(RecoveryTrigger trigger)
|
||||
{
|
||||
switch (trigger)
|
||||
{
|
||||
case RecoveryTrigger::kPlanningFailed:
|
||||
return recovery_core::RecoveryTrigger::kPlanningFailed;
|
||||
case RecoveryTrigger::kControllingFailed:
|
||||
return recovery_core::RecoveryTrigger::kControllingFailed;
|
||||
case RecoveryTrigger::kOscillation:
|
||||
return recovery_core::RecoveryTrigger::kOscillation;
|
||||
}
|
||||
return recovery_core::RecoveryTrigger::kUnspecified;
|
||||
}
|
||||
|
||||
RecoveryOutputKind toPortKind(recovery_core::RecoveryOutputType kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case recovery_core::RecoveryOutputType::kNone:
|
||||
return RecoveryOutputKind::kNone;
|
||||
case recovery_core::RecoveryOutputType::kVelocity:
|
||||
return RecoveryOutputKind::kVelocity;
|
||||
case recovery_core::RecoveryOutputType::kPath:
|
||||
return RecoveryOutputKind::kPath;
|
||||
}
|
||||
return RecoveryOutputKind::kNone;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void RecoveryRunner::setDeps(const Deps& deps)
|
||||
{
|
||||
deps_ = deps;
|
||||
pose_bridge_.setPort(deps_.pose);
|
||||
collision_.setCostmap(deps_.local_costmap);
|
||||
}
|
||||
|
||||
void RecoveryRunner::setCostmaps(robot_costmap_2d::Costmap2DROBOT* local,
|
||||
robot_costmap_2d::Costmap2DROBOT* global)
|
||||
{
|
||||
deps_.local_costmap = local;
|
||||
deps_.global_costmap = global;
|
||||
collision_.setCostmap(local);
|
||||
}
|
||||
|
||||
void RecoveryRunner::setNamespace(const std::string& ns)
|
||||
{
|
||||
namespace_ = ns;
|
||||
}
|
||||
|
||||
void RecoveryRunner::setPlanSource(PlanSource source)
|
||||
{
|
||||
plan_bridge_.setSource(std::move(source));
|
||||
}
|
||||
|
||||
void RecoveryRunner::refreshContext()
|
||||
{
|
||||
// Trỏ lại mỗi lượt thay vì tin bản cache từ configure(): con trỏ costmap là non-owning và có thể
|
||||
// bị thay khi runtime dựng lại costmap. Cache nó chính là nguyên nhân lỗi double-free đã ghi nhận
|
||||
// trong workspace.
|
||||
collision_.setCostmap(deps_.local_costmap);
|
||||
|
||||
ctx_.pose = &pose_bridge_;
|
||||
ctx_.collision = &collision_;
|
||||
ctx_.plan = &plan_bridge_;
|
||||
ctx_.local_costmap = deps_.local_costmap;
|
||||
ctx_.global_costmap = deps_.global_costmap;
|
||||
}
|
||||
|
||||
bool RecoveryRunner::configure(robot::NodeHandle& nh)
|
||||
{
|
||||
if (configured_)
|
||||
{
|
||||
robot::log_error("[move_base2] RecoveryRunner: configure() gọi lần thứ hai.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (deps_.clock == nullptr || deps_.pose == nullptr)
|
||||
{
|
||||
robot::log_error("[move_base2] RecoveryRunner: thiếu ClockPort hoặc PosePort.");
|
||||
return false;
|
||||
}
|
||||
|
||||
refreshContext();
|
||||
|
||||
const bool all_ok = registry_.loadFromConfig(nh, namespace_, ctx_);
|
||||
|
||||
if (registry_.size() == 0)
|
||||
{
|
||||
robot::log_error("[move_base2] RecoveryRunner: không nạp được behavior nào từ namespace '%s' — "
|
||||
"runtime sẽ không có đường phục hồi.", namespace_.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
configured_ = true;
|
||||
|
||||
if (!all_ok)
|
||||
{
|
||||
// Một số behavior hỏng nhưng phần còn lại dùng được: giữ chúng lại và báo false để bên gọi
|
||||
// quyết định (chạy tiếp với ít đường phục hồi hơn, hay dừng khởi động).
|
||||
robot::log_warning("[move_base2] RecoveryRunner: nạp được %zu behavior, một số entry bị bỏ.",
|
||||
registry_.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
robot::log_info("[move_base2] RecoveryRunner: nạp %zu recovery behavior từ '%s'.",
|
||||
registry_.size(), namespace_.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t RecoveryRunner::behaviorCount() const
|
||||
{
|
||||
return registry_.size();
|
||||
}
|
||||
|
||||
RecoveryOutputKind RecoveryRunner::outputKind(std::size_t index) const
|
||||
{
|
||||
const recovery_core::RecoveryBehavior* behavior = registry_.at(index);
|
||||
// Index sai -> kNone: không cấp quyền phát vận tốc cho thứ không biết là gì.
|
||||
return behavior == nullptr ? RecoveryOutputKind::kNone : toPortKind(behavior->outputKind());
|
||||
}
|
||||
|
||||
std::string RecoveryRunner::behaviorName(std::size_t index) const
|
||||
{
|
||||
return registry_.nameAt(index);
|
||||
}
|
||||
|
||||
bool RecoveryRunner::start(std::size_t index, RecoveryTrigger trigger)
|
||||
{
|
||||
active_ = nullptr;
|
||||
|
||||
if (!configured_)
|
||||
{
|
||||
robot::log_error("[move_base2] RecoveryRunner: start() trước configure().");
|
||||
return false;
|
||||
}
|
||||
|
||||
recovery_core::RecoveryBehavior* behavior = registry_.at(index);
|
||||
if (behavior == nullptr)
|
||||
{
|
||||
robot::log_error("[move_base2] RecoveryRunner: index %zu ngoài dải (%zu behavior).", index,
|
||||
registry_.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
refreshContext();
|
||||
|
||||
recovery_core::RecoveryGoal goal;
|
||||
goal.trigger = toCoreTrigger(trigger);
|
||||
// Không đặt angle/distance: để behavior dùng default đã cấu hình của nó. Lõi chưa có nguồn thông
|
||||
// tin nào để chọn góc/quãng tốt hơn config; khi có (ví dụ hình học vật cản), đặt vào đây.
|
||||
|
||||
if (!behavior->start(goal, deps_.clock->now()))
|
||||
{
|
||||
robot::log_warning("[move_base2] RecoveryRunner: behavior '%s' từ chối khởi động (%s).",
|
||||
registry_.nameAt(index).c_str(), toString(trigger));
|
||||
return false;
|
||||
}
|
||||
|
||||
active_ = behavior;
|
||||
return true;
|
||||
}
|
||||
|
||||
RecoveryTick RecoveryRunner::update()
|
||||
{
|
||||
RecoveryTick tick;
|
||||
|
||||
if (active_ == nullptr)
|
||||
{
|
||||
// Contract nói update() chỉ được gọi sau start() trả true. Vẫn guard: state machine hỏng thì
|
||||
// phải thành "recovery này thất bại" chứ không phải dereference null.
|
||||
tick.status = RecoveryTick::Status::kFailed;
|
||||
tick.message = "update() khi không có behavior nào đang chạy";
|
||||
return tick;
|
||||
}
|
||||
|
||||
refreshContext();
|
||||
return toTick(active_->update(deps_.clock->now()));
|
||||
}
|
||||
|
||||
void RecoveryRunner::cancel()
|
||||
{
|
||||
if (active_ != nullptr)
|
||||
{
|
||||
active_->cancel();
|
||||
}
|
||||
}
|
||||
|
||||
RecoveryTick RecoveryRunner::toTick(const recovery_core::RecoveryResult& result) const
|
||||
{
|
||||
RecoveryTick tick;
|
||||
|
||||
switch (result.status)
|
||||
{
|
||||
case recovery_core::RecoveryStatus::kRunning:
|
||||
tick.status = RecoveryTick::Status::kRunning;
|
||||
break;
|
||||
case recovery_core::RecoveryStatus::kSucceeded:
|
||||
tick.status = RecoveryTick::Status::kSucceeded;
|
||||
break;
|
||||
case recovery_core::RecoveryStatus::kIdle:
|
||||
// Behavior chưa start mà đã bị tick — lỗi thứ tự gọi, không phải trạng thái bình thường.
|
||||
tick.status = RecoveryTick::Status::kFailed;
|
||||
break;
|
||||
case recovery_core::RecoveryStatus::kCancelled:
|
||||
// Nhánh này KHÔNG đạt tới được với state machine hiện tại: sau cancel() nó chuyển sang
|
||||
// CANCELLING, nơi tick_recovery bị ép false. Giữ nhánh lại làm hàng rào nếu sau này ai đó nới
|
||||
// điều kiện tick — bỏ đi thì kCancelled sẽ rơi vào default và im lặng thành kRunning.
|
||||
tick.status = RecoveryTick::Status::kFailed;
|
||||
break;
|
||||
case recovery_core::RecoveryStatus::kFailed:
|
||||
tick.status = RecoveryTick::Status::kFailed;
|
||||
break;
|
||||
}
|
||||
|
||||
if (const robot_geometry_msgs::Twist* cmd = result.velocity())
|
||||
{
|
||||
tick.has_velocity = true;
|
||||
tick.cmd = *cmd;
|
||||
}
|
||||
|
||||
if (const robot_nav_msgs::Path* path = result.pathOut())
|
||||
{
|
||||
if (!path->poses.empty())
|
||||
{
|
||||
tick.has_path = true;
|
||||
tick.path = path->poses;
|
||||
}
|
||||
}
|
||||
|
||||
tick.message = result.message;
|
||||
return tick;
|
||||
}
|
||||
|
||||
} // namespace move_base2
|
||||
578
src/state_machine.cpp
Normal file
578
src/state_machine.cpp
Normal file
@@ -0,0 +1,578 @@
|
||||
/*********************************************************************
|
||||
*
|
||||
* Software License Agreement (BSD License)
|
||||
*
|
||||
* move_base2 — cài đặt state machine. Bảng chuyển đầy đủ, không I/O, không logging.
|
||||
*
|
||||
* Author: DuongTD
|
||||
*********************************************************************/
|
||||
#include <move_base2/core/state_machine.h>
|
||||
|
||||
#include <sstream>
|
||||
|
||||
namespace move_base2
|
||||
{
|
||||
|
||||
// ================================================================================================
|
||||
// StateMachineConfig
|
||||
// ================================================================================================
|
||||
|
||||
bool StateMachineConfig::validate(std::string& error) const
|
||||
{
|
||||
// Các ngưỡng thời gian được phép <= 0 với nghĩa "tắt", nên không kiểm dấu ở đây. Thứ phải chặn là
|
||||
// giá trị vô nghĩa: khoảng cách chống quẩn âm, và bật chống quẩn mà không cho khoảng cách nào.
|
||||
if (oscillation_distance < 0.0)
|
||||
{
|
||||
error = "oscillation_distance phải >= 0 [m]";
|
||||
return false;
|
||||
}
|
||||
if (oscillation_timeout > 0.0 && oscillation_distance <= 0.0)
|
||||
{
|
||||
error = "bật oscillation_timeout thì oscillation_distance phải > 0 [m], nếu không mọi cycle "
|
||||
"đều bị coi là quẩn";
|
||||
return false;
|
||||
}
|
||||
if (planner_patience <= 0.0 && max_planning_retries < 0)
|
||||
{
|
||||
// Từ khi lập plan chạy trên thread riêng, hai tham số này là thứ DUY NHẤT phát hiện được planner
|
||||
// treo. Tắt cả hai nghĩa là một plugin không bao giờ trả lời sẽ giữ robot ở PLANNING vĩnh viễn,
|
||||
// im lặng, và state machine tin rằng mọi thứ bình thường. Ở chế độ đồng bộ trước đây điều này
|
||||
// vô hại hơn nhiều vì planner treo làm treo luôn control loop — hỏng thì thấy ngay.
|
||||
error = "planner_patience <= 0 [s] và max_planning_retries < 0 cùng lúc: không có gì phát hiện "
|
||||
"được planner treo; đặt ít nhất một trong hai";
|
||||
return false;
|
||||
}
|
||||
if (recovery_enabled && recovery_behavior_count == 0)
|
||||
{
|
||||
// Không phải lỗi cấu hình chết người, nhưng để im lặng thì lúc chạy sẽ ABORTED ngay ở lỗi đầu
|
||||
// tiên mà không ai hiểu vì sao. Bắt buộc khai báo tường minh recovery_enabled = false.
|
||||
error = "recovery_enabled = true nhưng recovery_behavior_count = 0; đặt recovery_enabled = "
|
||||
"false nếu thực sự không muốn có recovery";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string StateMachineConfig::describe() const
|
||||
{
|
||||
std::ostringstream out;
|
||||
out << "StateMachineConfig:\n";
|
||||
out << " planner_patience : " << planner_patience << " s"
|
||||
<< (planner_patience > 0.0 ? "" : " (tắt)") << '\n';
|
||||
out << " controller_patience : " << controller_patience << " s"
|
||||
<< (controller_patience > 0.0 ? "" : " (tắt)") << '\n';
|
||||
out << " oscillation_timeout : " << oscillation_timeout << " s"
|
||||
<< (oscillation_timeout > 0.0 ? "" : " (tắt)") << '\n';
|
||||
out << " action_patience : " << action_patience << " s"
|
||||
<< (action_patience > 0.0 ? "" : " (tắt — handler tự timeout)") << '\n';
|
||||
out << " oscillation_distance : " << oscillation_distance << " m\n";
|
||||
out << " max_planning_retries : " << max_planning_retries
|
||||
<< (max_planning_retries < 0 ? " (không giới hạn)" : "") << '\n';
|
||||
out << " recovery_enabled : " << (recovery_enabled ? "true" : "false") << '\n';
|
||||
out << " recovery_behavior_cnt : " << recovery_behavior_count << '\n';
|
||||
return out.str();
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// StateMachine
|
||||
// ================================================================================================
|
||||
|
||||
bool StateMachine::configure(const StateMachineConfig& config, std::string& error)
|
||||
{
|
||||
if (!config.validate(error))
|
||||
{
|
||||
initialized_ = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
config_ = config;
|
||||
initialized_ = true;
|
||||
reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
void StateMachine::reset()
|
||||
{
|
||||
state_ = NavigationState::kIdle;
|
||||
state_before_pause_ = NavigationState::kIdle;
|
||||
state_entered_at_ = robot::Time();
|
||||
last_valid_plan_ = robot::Time();
|
||||
last_valid_control_ = robot::Time();
|
||||
last_oscillation_reset_ = robot::Time();
|
||||
recovery_index_ = 0;
|
||||
planning_retries_ = 0;
|
||||
request_has_goal_ = true;
|
||||
action_count_ = 0;
|
||||
action_index_ = 0;
|
||||
action_started_at_ = robot::Time();
|
||||
}
|
||||
|
||||
double StateMachine::secondsInState(const robot::Time& now) const
|
||||
{
|
||||
return (now - state_entered_at_).toSec();
|
||||
}
|
||||
|
||||
void StateMachine::enter(NavigationState next, const robot::Time& now, const char* reason,
|
||||
StateMachineOutput& out)
|
||||
{
|
||||
if (next != state_)
|
||||
{
|
||||
state_ = next;
|
||||
state_entered_at_ = now;
|
||||
out.state_changed = true;
|
||||
}
|
||||
out.state = state_;
|
||||
out.reason = reason;
|
||||
}
|
||||
|
||||
void StateMachine::beginPlanningCycle(const robot::Time& now)
|
||||
{
|
||||
last_valid_plan_ = now;
|
||||
planning_retries_ = 0;
|
||||
}
|
||||
|
||||
void StateMachine::escalateToRecovery(RecoveryTrigger trigger, const robot::Time& now,
|
||||
const char* reason, StateMachineOutput& out)
|
||||
{
|
||||
if (!config_.recovery_enabled || recovery_index_ >= config_.recovery_behavior_count)
|
||||
{
|
||||
finish(NavigationState::kAborted, NavigationOutcome::kFailed, now,
|
||||
"hết recovery behavior khả dụng", out);
|
||||
return;
|
||||
}
|
||||
|
||||
out.start_recovery = true;
|
||||
out.recovery_index = recovery_index_;
|
||||
out.recovery_trigger = trigger;
|
||||
enter(NavigationState::kRecovering, now, reason, out);
|
||||
}
|
||||
|
||||
void StateMachine::finish(NavigationState terminal, NavigationOutcome outcome,
|
||||
const robot::Time& now, const char* reason, StateMachineOutput& out)
|
||||
{
|
||||
// Cờ này bật đúng tại cycle bước vào state terminal, và state terminal chỉ tồn tại một cycle
|
||||
// (cycle sau đã về kIdle). Đó là toàn bộ cơ chế giữ bất biến "báo kết quả đúng một lần".
|
||||
out.report_outcome = true;
|
||||
out.outcome = outcome;
|
||||
out.stop_planner = true;
|
||||
enter(terminal, now, reason, out);
|
||||
}
|
||||
|
||||
StateMachineOutput StateMachine::update(const StateMachineInput& in)
|
||||
{
|
||||
StateMachineOutput out;
|
||||
out.state = state_;
|
||||
out.recovery_index = recovery_index_;
|
||||
|
||||
if (!initialized_)
|
||||
{
|
||||
// Guard bắt buộc: không bao giờ quyết định điều khiển khi chưa configure.
|
||||
out.state = NavigationState::kIdle;
|
||||
out.velocity_source = VelocitySource::kNone;
|
||||
out.reason = "chưa configure";
|
||||
return out;
|
||||
}
|
||||
|
||||
// State terminal chỉ sống một cycle. Về kIdle ngay đầu cycle kế tiếp để yêu cầu mới được nhận
|
||||
// không phải chờ thêm một vòng.
|
||||
if (isTerminal(state_))
|
||||
{
|
||||
enter(NavigationState::kIdle, in.now, "yêu cầu đã kết thúc", out);
|
||||
}
|
||||
|
||||
// Mất pose nghĩa là không biết robot ở đâu. Khi đó controller không được chạy, và không nguồn nào
|
||||
// được phát vận tốc — kể cả recovery. Recovery vẫn được tick để nó tự báo lỗi theo contract của
|
||||
// nó, nhưng lệnh nó sinh ra bị chặn ở phần chốt bất biến cuối hàm.
|
||||
const ControllerFeedback controller =
|
||||
in.pose_available ? in.controller : ControllerFeedback::kNoValidCommand;
|
||||
|
||||
switch (state_)
|
||||
{
|
||||
// --------------------------------------------------------------------------------------
|
||||
case NavigationState::kIdle:
|
||||
{
|
||||
if (in.has_pending_request)
|
||||
{
|
||||
out.accept_request = true;
|
||||
recovery_index_ = 0;
|
||||
request_has_goal_ = in.pending_request_has_goal;
|
||||
action_count_ = in.pending_request_action_count;
|
||||
action_index_ = 0;
|
||||
|
||||
if (!request_has_goal_)
|
||||
{
|
||||
// D8: yêu cầu chỉ-có-action — không có gì để lập plan, vào thẳng thực thi action.
|
||||
if (action_count_ == 0)
|
||||
{
|
||||
// Không goal lẫn action là vi phạm contract; mission layer đã validate nhưng lõi vẫn
|
||||
// phải tự vệ: kết thúc tường minh thay vì treo ở một state không có đường ra.
|
||||
finish(NavigationState::kAborted, NavigationOutcome::kFailed, in.now,
|
||||
"yêu cầu không có goal lẫn action", out);
|
||||
break;
|
||||
}
|
||||
out.start_action = true;
|
||||
out.action_index = 0;
|
||||
action_started_at_ = in.now;
|
||||
enter(NavigationState::kExecutingActions, in.now, "yêu cầu chỉ có action", out);
|
||||
break;
|
||||
}
|
||||
|
||||
out.start_planner = true;
|
||||
beginPlanningCycle(in.now);
|
||||
last_valid_control_ = in.now;
|
||||
last_oscillation_reset_ = in.now;
|
||||
out.reset_oscillation_origin = true;
|
||||
enter(NavigationState::kPlanning, in.now, "nhận yêu cầu mới", out);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
case NavigationState::kPlanning:
|
||||
{
|
||||
if (in.cancel_requested)
|
||||
{
|
||||
out.stop_planner = true;
|
||||
enter(NavigationState::kCancelling, in.now, "huỷ khi đang lập plan", out);
|
||||
break;
|
||||
}
|
||||
if (in.pause_requested)
|
||||
{
|
||||
out.stop_planner = true;
|
||||
state_before_pause_ = NavigationState::kPlanning;
|
||||
enter(NavigationState::kPaused, in.now, "tạm dừng khi đang lập plan", out);
|
||||
break;
|
||||
}
|
||||
|
||||
if (in.planner == PlannerFeedback::kPlanReady)
|
||||
{
|
||||
out.apply_plan = true;
|
||||
out.run_controller = true; // Chạy controller ngay trong cycle này, không phí một vòng.
|
||||
beginPlanningCycle(in.now);
|
||||
|
||||
// Cố ý KHÔNG làm mới last_valid_control_ và last_oscillation_reset_ ở đây. Có plan mới
|
||||
// không chứng minh được gì về controller: nếu reset thì vòng lặp
|
||||
// CONTROLLING -> PLANNING -> CONTROLLING sẽ làm mới đồng hồ mỗi vòng, và một controller
|
||||
// hỏng vĩnh viễn sẽ không bao giờ chạm controller_patience. Hai đồng hồ đó chỉ được đặt lại
|
||||
// ở ba chỗ: nhận yêu cầu mới, tiếp tục sau tạm dừng, và sau khi recovery chạy xong.
|
||||
enter(NavigationState::kControlling, in.now, "có plan hợp lệ", out);
|
||||
break;
|
||||
}
|
||||
|
||||
if (in.planner == PlannerFeedback::kFailed)
|
||||
{
|
||||
++planning_retries_;
|
||||
}
|
||||
|
||||
const bool retries_exhausted = config_.max_planning_retries >= 0 &&
|
||||
planning_retries_ > config_.max_planning_retries;
|
||||
const bool patience_exhausted =
|
||||
config_.planner_patience > 0.0 &&
|
||||
(in.now - last_valid_plan_).toSec() > config_.planner_patience;
|
||||
|
||||
if (retries_exhausted || patience_exhausted)
|
||||
{
|
||||
out.stop_planner = true;
|
||||
escalateToRecovery(RecoveryTrigger::kPlanningFailed, in.now,
|
||||
retries_exhausted ? "hết lượt lập plan" : "quá hạn lập plan", out);
|
||||
break;
|
||||
}
|
||||
|
||||
out.start_planner = true; // Giữ planner chạy tiếp.
|
||||
break;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
case NavigationState::kControlling:
|
||||
{
|
||||
if (in.cancel_requested)
|
||||
{
|
||||
out.stop_planner = true;
|
||||
enter(NavigationState::kCancelling, in.now, "huỷ khi đang bám plan", out);
|
||||
break;
|
||||
}
|
||||
if (in.pause_requested)
|
||||
{
|
||||
out.stop_planner = true;
|
||||
state_before_pause_ = NavigationState::kControlling;
|
||||
enter(NavigationState::kPaused, in.now, "tạm dừng khi đang bám plan", out);
|
||||
break;
|
||||
}
|
||||
|
||||
// Plan mới tới giữa lúc đang bám plan cũ: nhận ngay, vẫn ở kControlling.
|
||||
if (in.planner == PlannerFeedback::kPlanReady)
|
||||
{
|
||||
out.apply_plan = true;
|
||||
beginPlanningCycle(in.now);
|
||||
}
|
||||
|
||||
// Đi đủ xa thì không còn bị coi là quẩn — đặt lại cả đồng hồ lẫn mốc đo quãng đường.
|
||||
if (config_.oscillation_distance > 0.0 &&
|
||||
in.travelled_since_oscillation_reset >= config_.oscillation_distance)
|
||||
{
|
||||
last_oscillation_reset_ = in.now;
|
||||
out.reset_oscillation_origin = true;
|
||||
}
|
||||
|
||||
if (controller == ControllerFeedback::kGoalReached)
|
||||
{
|
||||
if (action_count_ > 0)
|
||||
{
|
||||
// D8: tới goal chưa phải là xong — mission còn action phải chạy tại chỗ. Kết quả chỉ
|
||||
// được báo sau action cuối, để mission layer thấy trọn một chặng nav + action.
|
||||
out.stop_planner = true;
|
||||
out.start_action = true;
|
||||
out.action_index = action_index_;
|
||||
action_started_at_ = in.now;
|
||||
enter(NavigationState::kExecutingActions, in.now, "đạt goal, còn action phải chạy", out);
|
||||
break;
|
||||
}
|
||||
finish(NavigationState::kSucceeded, NavigationOutcome::kSucceeded, in.now, "đạt goal", out);
|
||||
break;
|
||||
}
|
||||
|
||||
if (controller == ControllerFeedback::kCommandValid)
|
||||
{
|
||||
last_valid_control_ = in.now;
|
||||
|
||||
if (config_.oscillation_timeout > 0.0 &&
|
||||
(in.now - last_oscillation_reset_).toSec() > config_.oscillation_timeout)
|
||||
{
|
||||
out.stop_planner = true;
|
||||
escalateToRecovery(RecoveryTrigger::kOscillation, in.now, "quẩn tại chỗ quá lâu", out);
|
||||
break;
|
||||
}
|
||||
|
||||
out.run_controller = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Còn lại: kIdle (cycle đầu sau khi vào state) hoặc kNoValidCommand.
|
||||
if (config_.controller_patience > 0.0 &&
|
||||
(in.now - last_valid_control_).toSec() > config_.controller_patience)
|
||||
{
|
||||
out.stop_planner = true;
|
||||
escalateToRecovery(RecoveryTrigger::kControllingFailed, in.now,
|
||||
"quá hạn sinh lệnh vận tốc", out);
|
||||
break;
|
||||
}
|
||||
|
||||
if (controller == ControllerFeedback::kNoValidCommand && in.pose_available)
|
||||
{
|
||||
// Chưa hết kiên nhẫn: quay lại lập plan. Cố ý KHÔNG reset last_valid_control_ ở đây, nếu
|
||||
// không thì vòng lặp lập-plan-rồi-lại-hỏng sẽ không bao giờ chạm controller_patience.
|
||||
out.start_planner = true;
|
||||
beginPlanningCycle(in.now);
|
||||
enter(NavigationState::kPlanning, in.now, "controller không sinh được lệnh, lập lại plan",
|
||||
out);
|
||||
break;
|
||||
}
|
||||
|
||||
// Mất pose thì ở nguyên kControlling (vận tốc đã bị ép về 0 ở phần chốt bất biến) cho tới khi
|
||||
// controller_patience hết hạn. Lập lại plan không giúp được gì khi vấn đề là định vị, và
|
||||
// nhảy sang kPlanning chỉ làm lý do vào recovery bị ghi nhận sai thành "lập plan hỏng".
|
||||
|
||||
out.run_controller = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
case NavigationState::kRecovering:
|
||||
{
|
||||
if (in.cancel_requested)
|
||||
{
|
||||
out.cancel_recovery = true;
|
||||
enter(NavigationState::kCancelling, in.now, "huỷ khi đang recovery", out);
|
||||
break;
|
||||
}
|
||||
if (in.pause_requested)
|
||||
{
|
||||
// Recovery bị huỷ khi tạm dừng: giữ một behavior ở trạng thái dở dang qua một quãng dừng
|
||||
// dài là không an toàn (nó dead-reckon theo thời gian). Resume sẽ lập plan lại từ đầu.
|
||||
out.cancel_recovery = true;
|
||||
state_before_pause_ = NavigationState::kPlanning;
|
||||
enter(NavigationState::kPaused, in.now, "tạm dừng khi đang recovery", out);
|
||||
break;
|
||||
}
|
||||
|
||||
if (in.recovery == RecoveryFeedback::kSucceeded || in.recovery == RecoveryFeedback::kFailed)
|
||||
{
|
||||
// Behavior chạy xong (thành công hay không) thì thử lập plan lại. Lỗi kế tiếp sẽ dùng
|
||||
// behavior kế tiếp; hết behavior thì ABORTED.
|
||||
++recovery_index_;
|
||||
out.start_planner = true;
|
||||
beginPlanningCycle(in.now);
|
||||
last_valid_control_ = in.now;
|
||||
enter(NavigationState::kPlanning, in.now,
|
||||
in.recovery == RecoveryFeedback::kSucceeded ? "recovery xong, lập plan lại"
|
||||
: "recovery thất bại, lập plan lại",
|
||||
out);
|
||||
break;
|
||||
}
|
||||
|
||||
out.tick_recovery = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
case NavigationState::kExecutingActions:
|
||||
{
|
||||
if (in.cancel_requested)
|
||||
{
|
||||
out.cancel_action = true;
|
||||
enter(NavigationState::kCancelling, in.now, "huỷ khi đang chạy action", out);
|
||||
break;
|
||||
}
|
||||
if (in.pause_requested)
|
||||
{
|
||||
// Khác recovery, action KHÔNG bị huỷ khi tạm dừng: robot đang đứng yên nên không có rủi ro
|
||||
// dead-reckon, còn chạy lại một action thiết bị (nâng/hạ, sạc) từ đầu thì không chắc an
|
||||
// toàn — action không idempotent. Tạm dừng chỉ ngừng tick; resume tick tiếp đúng action đó.
|
||||
state_before_pause_ = NavigationState::kExecutingActions;
|
||||
enter(NavigationState::kPaused, in.now, "tạm dừng khi đang chạy action", out);
|
||||
break;
|
||||
}
|
||||
|
||||
if (in.action == ActionFeedback::kFailed)
|
||||
{
|
||||
// Action hỏng không có đường recovery: recovery behavior là công cụ phục hồi NAVIGATION
|
||||
// (dọn costmap, lùi, xoay), không giúp gì được một thiết bị đang hỏng. Kết thúc tường minh
|
||||
// để mission layer quyết định làm gì với phần còn lại của order.
|
||||
finish(NavigationState::kAborted, NavigationOutcome::kFailed, in.now, "action thất bại",
|
||||
out);
|
||||
break;
|
||||
}
|
||||
|
||||
if (in.action == ActionFeedback::kSucceeded)
|
||||
{
|
||||
++action_index_;
|
||||
if (action_index_ < action_count_)
|
||||
{
|
||||
out.start_action = true;
|
||||
out.action_index = action_index_;
|
||||
action_started_at_ = in.now; // Trần thời gian tính cho TỪNG action, không cho cả chuỗi.
|
||||
break; // Vẫn ở kExecutingActions, chuyển sang action kế tiếp.
|
||||
}
|
||||
finish(NavigationState::kSucceeded, NavigationOutcome::kSucceeded, in.now,
|
||||
"action cuối đã xong", out);
|
||||
break;
|
||||
}
|
||||
|
||||
// Lưới an toàn cuối cùng cho action treo (handler hỏng, thiết bị câm lặng vĩnh viễn).
|
||||
// Cơ chế timeout CHÍNH là của từng ActionHandler; lưới này mặc định tắt.
|
||||
if (config_.action_patience > 0.0 &&
|
||||
(in.now - action_started_at_).toSec() > config_.action_patience)
|
||||
{
|
||||
out.cancel_action = true; // Bảo port dừng thiết bị an toàn trước khi kết thúc chặng.
|
||||
finish(NavigationState::kAborted, NavigationOutcome::kFailed, in.now,
|
||||
"action quá hạn action_patience", out);
|
||||
break;
|
||||
}
|
||||
|
||||
// Mất pose KHÔNG chặn tick action: robot đứng yên, thao tác thiết bị không cần định vị.
|
||||
// Vận tốc vẫn bị ép về 0 ở phần chốt bất biến cuối hàm như mọi state phải dừng khác.
|
||||
out.tick_action = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
case NavigationState::kPaused:
|
||||
{
|
||||
if (in.cancel_requested)
|
||||
{
|
||||
enter(NavigationState::kCancelling, in.now, "huỷ khi đang tạm dừng", out);
|
||||
break;
|
||||
}
|
||||
if (in.resume_requested)
|
||||
{
|
||||
// Đặt lại toàn bộ đồng hồ kiên nhẫn: một lần tạm dừng dài không được tính là "planner chậm"
|
||||
// hay "controller hỏng", nếu không thì resume xong là rơi thẳng vào recovery.
|
||||
beginPlanningCycle(in.now);
|
||||
last_valid_control_ = in.now;
|
||||
last_oscillation_reset_ = in.now;
|
||||
out.reset_oscillation_origin = true;
|
||||
|
||||
if (state_before_pause_ == NavigationState::kControlling)
|
||||
{
|
||||
out.run_controller = true;
|
||||
enter(NavigationState::kControlling, in.now, "tiếp tục bám plan", out);
|
||||
}
|
||||
else if (state_before_pause_ == NavigationState::kExecutingActions)
|
||||
{
|
||||
// Action không bị huỷ khi tạm dừng nên không start lại — tick tiếp đúng action dở dang.
|
||||
// Mốc action_patience được gieo lại: một quãng dừng dài không được tính vào trần thời
|
||||
// gian của action, nếu không resume xong là ABORTED oan ngay lập tức.
|
||||
action_started_at_ = in.now;
|
||||
out.tick_action = true;
|
||||
enter(NavigationState::kExecutingActions, in.now, "tiếp tục chạy action", out);
|
||||
}
|
||||
else
|
||||
{
|
||||
out.start_planner = true;
|
||||
enter(NavigationState::kPlanning, in.now, "tiếp tục lập plan", out);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
case NavigationState::kCancelling:
|
||||
{
|
||||
// Nguồn vận tốc đã là kNone nên bộ trọng tài đang giảm tốc về 0 theo trần gia tốc; trạng thái
|
||||
// này vì vậy luôn kết thúc sau hữu hạn cycle, không cần thêm timeout.
|
||||
if (in.robot_stopped)
|
||||
{
|
||||
finish(NavigationState::kCancelled, NavigationOutcome::kCancelled, in.now,
|
||||
"robot đã dừng hẳn", out);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
case NavigationState::kSucceeded:
|
||||
case NavigationState::kAborted:
|
||||
case NavigationState::kCancelled:
|
||||
// Không tới được: đã chuyển về kIdle ở đầu hàm.
|
||||
break;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Chốt bất biến trước khi trả ra. Ba dòng này là hàng rào an toàn cuối cùng của lõi.
|
||||
// ------------------------------------------------------------------------------------------
|
||||
out.state = state_;
|
||||
|
||||
if (state_ == NavigationState::kControlling)
|
||||
{
|
||||
out.velocity_source = VelocitySource::kController;
|
||||
}
|
||||
else if (state_ == NavigationState::kRecovering)
|
||||
{
|
||||
// Chỉ behavior thật sự lái robot mới được cấp quyền phát vận tốc. Behavior one-shot (đợi, xoá
|
||||
// costmap) giữ nguồn ở kNone, nên arbiter không phải đổi nguồn hai lần cho một lượt không có
|
||||
// vận tốc nào — mỗi lần đổi nguồn tốn một cycle zero (mục 1.6).
|
||||
out.velocity_source = in.active_recovery_output == RecoveryOutputKind::kVelocity
|
||||
? VelocitySource::kRecovery
|
||||
: VelocitySource::kNone;
|
||||
}
|
||||
else
|
||||
{
|
||||
out.velocity_source = VelocitySource::kNone;
|
||||
}
|
||||
|
||||
if (!in.pose_available)
|
||||
{
|
||||
out.velocity_source = VelocitySource::kNone;
|
||||
out.run_controller = false;
|
||||
}
|
||||
|
||||
if (mustBeStopped(state_))
|
||||
{
|
||||
out.velocity_source = VelocitySource::kNone;
|
||||
out.run_controller = false;
|
||||
out.tick_recovery = false;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace move_base2
|
||||
245
src/velocity_arbiter.cpp
Normal file
245
src/velocity_arbiter.cpp
Normal file
@@ -0,0 +1,245 @@
|
||||
/*********************************************************************
|
||||
*
|
||||
* Software License Agreement (BSD License)
|
||||
*
|
||||
* move_base2 — cài đặt bộ trọng tài vận tốc.
|
||||
*
|
||||
* Author: DuongTD
|
||||
*********************************************************************/
|
||||
#include <move_base2/core/velocity_arbiter.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
|
||||
namespace move_base2
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
/// @brief Giá trị hữu hạn (không NaN, không Inf).
|
||||
bool isFinite(double value)
|
||||
{
|
||||
return std::isfinite(value);
|
||||
}
|
||||
|
||||
/// @brief Đưa @p value về [lower, upper]. Trả true qua @p clamped nếu có cắt.
|
||||
double clampTo(double value, double lower, double upper, bool& clamped)
|
||||
{
|
||||
if (value < lower)
|
||||
{
|
||||
clamped = true;
|
||||
return lower;
|
||||
}
|
||||
if (value > upper)
|
||||
{
|
||||
clamped = true;
|
||||
return upper;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ================================================================================================
|
||||
// VelocityLimits
|
||||
// ================================================================================================
|
||||
|
||||
bool VelocityLimits::validate(std::string& error) const
|
||||
{
|
||||
if (!(max_vel_x > 0.0))
|
||||
{
|
||||
error = "max_vel_x phải > 0 [m/s]";
|
||||
return false;
|
||||
}
|
||||
if (min_vel_x > 0.0)
|
||||
{
|
||||
error = "min_vel_x là trần tốc độ LÙI nên phải <= 0 [m/s]; đặt 0 nếu cấm lùi";
|
||||
return false;
|
||||
}
|
||||
if (!(max_vel_theta > 0.0))
|
||||
{
|
||||
error = "max_vel_theta phải > 0 [rad/s]";
|
||||
return false;
|
||||
}
|
||||
if (!(max_accel_x > 0.0))
|
||||
{
|
||||
error = "max_accel_x phải > 0 [m/s^2]";
|
||||
return false;
|
||||
}
|
||||
if (!(max_accel_theta > 0.0))
|
||||
{
|
||||
error = "max_accel_theta phải > 0 [rad/s^2]";
|
||||
return false;
|
||||
}
|
||||
if (zero_velocity_epsilon < 0.0)
|
||||
{
|
||||
error = "zero_velocity_epsilon phải >= 0";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string VelocityLimits::describe() const
|
||||
{
|
||||
std::ostringstream out;
|
||||
out << "VelocityLimits:\n";
|
||||
out << " max_vel_x : " << max_vel_x << " m/s (tiến)\n";
|
||||
out << " min_vel_x : " << min_vel_x << " m/s (lùi"
|
||||
<< (min_vel_x == 0.0 ? ", đang cấm lùi" : "") << ")\n";
|
||||
out << " max_vel_theta : " << max_vel_theta << " rad/s\n";
|
||||
out << " max_accel_x : " << max_accel_x << " m/s^2\n";
|
||||
out << " max_accel_theta : " << max_accel_theta << " rad/s^2\n";
|
||||
out << " zero_velocity_epsilon: " << zero_velocity_epsilon << '\n';
|
||||
return out.str();
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// VelocityArbiter
|
||||
// ================================================================================================
|
||||
|
||||
robot_geometry_msgs::Twist VelocityArbiter::zeroTwist()
|
||||
{
|
||||
return robot_geometry_msgs::Twist();
|
||||
}
|
||||
|
||||
bool VelocityArbiter::configure(const VelocityLimits& limits, std::string& error)
|
||||
{
|
||||
if (!limits.validate(error))
|
||||
{
|
||||
initialized_ = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
limits_ = limits;
|
||||
initialized_ = true;
|
||||
reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
void VelocityArbiter::reset()
|
||||
{
|
||||
active_source_ = VelocitySource::kNone;
|
||||
last_command_ = zeroTwist();
|
||||
non_finite_rejections_ = 0;
|
||||
velocity_clamps_ = 0;
|
||||
acceleration_clamps_ = 0;
|
||||
handover_cycles_ = 0;
|
||||
}
|
||||
|
||||
bool VelocityArbiter::stopped() const
|
||||
{
|
||||
return std::abs(last_command_.linear.x) <= limits_.zero_velocity_epsilon &&
|
||||
std::abs(last_command_.linear.y) <= limits_.zero_velocity_epsilon &&
|
||||
std::abs(last_command_.angular.z) <= limits_.zero_velocity_epsilon;
|
||||
}
|
||||
|
||||
robot_geometry_msgs::Twist VelocityArbiter::sanitize(const robot_geometry_msgs::Twist& candidate)
|
||||
{
|
||||
robot_geometry_msgs::Twist clean;
|
||||
|
||||
// NaN/Inf từ bất kỳ trục nào làm hỏng cả lệnh: không có cách nào "sửa một phần" một lệnh mà bộ
|
||||
// sinh ra nó đang ở trạng thái hỏng. Trả 0 và đếm lại để tầng trên phát hiện được.
|
||||
if (!isFinite(candidate.linear.x) || !isFinite(candidate.linear.y) ||
|
||||
!isFinite(candidate.linear.z) || !isFinite(candidate.angular.x) ||
|
||||
!isFinite(candidate.angular.y) || !isFinite(candidate.angular.z))
|
||||
{
|
||||
++non_finite_rejections_;
|
||||
return clean;
|
||||
}
|
||||
|
||||
bool clamped = false;
|
||||
clean.linear.x = clampTo(candidate.linear.x, limits_.min_vel_x, limits_.max_vel_x, clamped);
|
||||
clean.angular.z =
|
||||
clampTo(candidate.angular.z, -limits_.max_vel_theta, limits_.max_vel_theta, clamped);
|
||||
|
||||
// Robot của workspace là phi holonomic ở mức contract cmd_vel: mọi thành phần còn lại bị bỏ, cố
|
||||
// ý không truyền tiếp để tránh một plugin lạ đẩy ra trục mà tầng dưới không kiểm.
|
||||
clean.linear.y = 0.0;
|
||||
clean.linear.z = 0.0;
|
||||
clean.angular.x = 0.0;
|
||||
clean.angular.y = 0.0;
|
||||
|
||||
if (clamped)
|
||||
{
|
||||
++velocity_clamps_;
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
|
||||
robot_geometry_msgs::Twist VelocityArbiter::limitAcceleration(
|
||||
const robot_geometry_msgs::Twist& target, double dt)
|
||||
{
|
||||
if (dt <= 0.0)
|
||||
{
|
||||
// Không biết dt thật thì không có cơ sở nào để giới hạn gia tốc. Trả nguyên lệnh đã sanitize
|
||||
// thay vì bịa ra một chu kỳ danh nghĩa — bịa chính là lớp lỗi mà thiết kế này muốn tránh.
|
||||
return target;
|
||||
}
|
||||
|
||||
robot_geometry_msgs::Twist limited = target;
|
||||
bool clamped = false;
|
||||
|
||||
const double max_dx = limits_.max_accel_x * dt;
|
||||
const double max_dtheta = limits_.max_accel_theta * dt;
|
||||
|
||||
limited.linear.x = clampTo(target.linear.x, last_command_.linear.x - max_dx,
|
||||
last_command_.linear.x + max_dx, clamped);
|
||||
limited.angular.z = clampTo(target.angular.z, last_command_.angular.z - max_dtheta,
|
||||
last_command_.angular.z + max_dtheta, clamped);
|
||||
|
||||
if (clamped)
|
||||
{
|
||||
++acceleration_clamps_;
|
||||
}
|
||||
return limited;
|
||||
}
|
||||
|
||||
robot_geometry_msgs::Twist VelocityArbiter::arbitrate(VelocitySource source,
|
||||
const robot_geometry_msgs::Twist& candidate,
|
||||
double dt)
|
||||
{
|
||||
if (!initialized_)
|
||||
{
|
||||
// Guard bắt buộc: chưa configure thì không phát gì cả.
|
||||
last_command_ = zeroTwist();
|
||||
active_source_ = VelocitySource::kNone;
|
||||
return last_command_;
|
||||
}
|
||||
|
||||
if (source == VelocitySource::kNone)
|
||||
{
|
||||
// Lệnh 0 TỨC THÌ, không giảm tốc dần. Ba lý do, theo thứ tự quan trọng:
|
||||
// 1. Lệnh vận tốc là giá trị được chốt lại ở tầng dưới. Nếu control loop dừng giữa lúc đang
|
||||
// giảm tốc dần thì lệnh khác 0 cuối cùng còn nguyên hiệu lực và robot chạy tiếp.
|
||||
// 2. Bất biến "state phải dừng thì lệnh đúng bằng 0" trở thành kiểm được, không phải "gần 0".
|
||||
// 3. Giảm tốc theo động học là việc của bộ điều khiển bánh xe, nơi biết tải và ma sát thật.
|
||||
active_source_ = VelocitySource::kNone;
|
||||
last_command_ = zeroTwist();
|
||||
return last_command_;
|
||||
}
|
||||
|
||||
// Đổi nguồn: chèn đúng một cycle vận tốc 0 trước khi nguồn mới được phát. Hai bộ sinh lệnh giữ
|
||||
// trạng thái gia tốc riêng nên nối thẳng chúng lại gây giật. Cycle 0 này cũng là ranh giới rõ
|
||||
// ràng để soi log khi điều tra sự cố.
|
||||
if (active_source_ != VelocitySource::kNone && active_source_ != source)
|
||||
{
|
||||
++handover_cycles_;
|
||||
active_source_ = source;
|
||||
last_command_ = zeroTwist();
|
||||
return last_command_;
|
||||
}
|
||||
|
||||
active_source_ = source;
|
||||
last_command_ = limitAcceleration(sanitize(candidate), dt);
|
||||
return last_command_;
|
||||
}
|
||||
|
||||
robot_geometry_msgs::Twist VelocityArbiter::emergencyStop()
|
||||
{
|
||||
active_source_ = VelocitySource::kNone;
|
||||
last_command_ = zeroTwist();
|
||||
return last_command_;
|
||||
}
|
||||
|
||||
} // namespace move_base2
|
||||
Reference in New Issue
Block a user