update to windows

This commit is contained in:
2026-06-28 20:08:54 +07:00
parent 1bd09d5e8e
commit 829ce20fbc
7 changed files with 694 additions and 26 deletions

View File

@@ -262,3 +262,58 @@ else()
endif() endif()
endif() endif()
# ========================================================
# Unit Test
# ========================================================
if(CATKIN_ENABLE_TESTING)
find_package(catkin REQUIRED COMPONENTS
robot_costmap_2d
robot_nav_core
robot_nav_core2
robot_nav_msgs
robot_std_msgs
robot_geometry_msgs
robot_cpp
robot_tf3_geometry_msgs
robot_visualization_msgs
robot_nav_2d_utils
data_convert
)
message(STATUS "CATKIN_ENABLE_TESTING=${CATKIN_ENABLE_TESTING}")
if(COMMAND catkin_add_gtest)
message(STATUS "catkin_add_gtest exists")
else()
message(FATAL_ERROR "catkin_add_gtest NOT FOUND")
endif()
find_package(GTest REQUIRED)
add_executable(test_mission_adapters
src/test_mission_adapters.cpp
)
target_link_libraries(test_mission_adapters
mission_adapters
GTest::GTest
GTest::Main
pthread
${catkin_LIBRARIES}
)
if(TARGET test_mission_adapters)
target_link_libraries(test_mission_adapters
mission_adapters
${catkin_LIBRARIES}
)
target_include_directories(test_mission_adapters PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${catkin_INCLUDE_DIRS}
)
endif()
endif()

View File

@@ -33,12 +33,12 @@ namespace mission_adapters
QUEUED, QUEUED,
RUNNING, RUNNING,
PAUSED, PAUSED,
WAITING_ACTION,
RECOVERY, RECOVERY,
COMPLETED, COMPLETED,
FAILED, FAILED,
CANCELLED, CANCELLED,
EMERGENCY EMERGENCY,
CLEAR_EMERGENCY
}; };
enum class EventType enum class EventType
@@ -49,7 +49,8 @@ namespace mission_adapters
PAUSE, PAUSE,
RESUME, RESUME,
CANCEL, CANCEL,
EMERGENCY EMERGENCY,
CLEAR_EMERGENCY
}; };
enum class MissionType enum class MissionType
@@ -110,6 +111,7 @@ namespace mission_adapters
public: public:
void push(const Event& event); void push(const Event& event);
void push(Event&& event);
bool pop(Event& event); bool pop(Event& event);
void stop(); void stop();
void reset(); void reset();
@@ -152,6 +154,7 @@ namespace mission_adapters
void pause(); void pause();
void resume(); void resume();
void emergency(); void emergency();
void clearEmergency();
MissionState state() const; MissionState state() const;
@@ -182,6 +185,7 @@ namespace mission_adapters
void navDoneEvent(); void navDoneEvent();
void navFailedEvent(); void navFailedEvent();
void emergencyEvent(); void emergencyEvent();
void clearEmergencyEvent();
private: private:
void spin(); void spin();

View File

@@ -47,4 +47,5 @@
<run_depend>robot_visualization_msgs</run_depend> <run_depend>robot_visualization_msgs</run_depend>
<run_depend>robot_nav_2d_utils</run_depend> <run_depend>robot_nav_2d_utils</run_depend>
<run_depend>data_convert</run_depend> <run_depend>data_convert</run_depend>
<test_depend>gtest</test_depend>
</package> </package>

View File

@@ -1,3 +1,4 @@
//file mission_adapters.cpp
#include <algorithm> #include <algorithm>
#include <mission_adapters/mission_adapters.h> #include <mission_adapters/mission_adapters.h>
@@ -14,6 +15,13 @@ namespace mission_adapters
cv_.notify_one(); cv_.notify_one();
} }
void EventBus::push(Event&& event)
{
std::lock_guard<std::mutex> lock(mutex_);
queue_.push(std::move(event));
cv_.notify_one();
}
bool EventBus::pop(Event& event) bool EventBus::pop(Event& event)
{ {
std::unique_lock<std::mutex> lock(mutex_); std::unique_lock<std::mutex> lock(mutex_);
@@ -66,6 +74,8 @@ namespace mission_adapters
std::vector<std::shared_ptr<Mission>> missions; std::vector<std::shared_ptr<Mission>> missions;
std::vector<size_t> action_node_indices; std::vector<size_t> action_node_indices;
if(order.nodes.empty()) return missions;
for (size_t i = 0; i < order.nodes.size(); ++i) for (size_t i = 0; i < order.nodes.size(); ++i)
{ {
if (!order.nodes[i].actions.empty()) if (!order.nodes[i].actions.empty())
@@ -126,9 +136,7 @@ namespace mission_adapters
for (const auto& action : edge.actions) for (const auto& action : edge.actions)
{ {
Action ma; Action ma;
// FIX #6: Use action.sequenceId, not edge.sequenceId, ma.sequenceId = edge.sequenceId;
// so the sort below produces the correct execution order.
ma.sequenceId = action.sequenceId;
ma.type = ActionType::EDGE_ACTION; ma.type = ActionType::EDGE_ACTION;
ma.action = action; ma.action = action;
mission->actions.push_back(std::move(ma)); mission->actions.push_back(std::move(ma));
@@ -139,8 +147,7 @@ namespace mission_adapters
for (const auto& action : node.actions) for (const auto& action : node.actions)
{ {
Action ma; Action ma;
// FIX #6: Same fix for node actions. ma.sequenceId = node.sequenceId;
ma.sequenceId = action.sequenceId;
ma.type = ActionType::NODE_ACTION; ma.type = ActionType::NODE_ACTION;
ma.action = action; ma.action = action;
mission->actions.push_back(std::move(ma)); mission->actions.push_back(std::move(ma));
@@ -162,6 +169,12 @@ namespace mission_adapters
void MissionManager::submit(const std::vector<std::shared_ptr<Mission>>& missions) void MissionManager::submit(const std::vector<std::shared_ptr<Mission>>& missions)
{ {
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
if (!mission_queue_.empty())
{
mission_queue_ = {};
current_mission_.reset();
}
for (const auto& mission : missions) for (const auto& mission : missions)
mission_queue_.push(mission); mission_queue_.push(mission);
@@ -176,6 +189,7 @@ namespace mission_adapters
case MissionState::FAILED: case MissionState::FAILED:
case MissionState::CANCELLED: case MissionState::CANCELLED:
case MissionState::EMERGENCY: case MissionState::EMERGENCY:
case MissionState::CLEAR_EMERGENCY:
state_ = MissionState::QUEUED; state_ = MissionState::QUEUED;
break; break;
default: default:
@@ -188,10 +202,12 @@ namespace mission_adapters
{ {
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
if (state_ == MissionState::PAUSED || if (state_ == MissionState::IDLE ||
state_ == MissionState::PAUSED ||
state_ == MissionState::FAILED || state_ == MissionState::FAILED ||
state_ == MissionState::CANCELLED || state_ == MissionState::CANCELLED ||
state_ == MissionState::EMERGENCY) state_ == MissionState::EMERGENCY ||
state_ == MissionState::CLEAR_EMERGENCY)
{ {
return nullptr; return nullptr;
} }
@@ -231,6 +247,7 @@ namespace mission_adapters
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
current_mission_.reset(); current_mission_.reset();
while (!mission_queue_.empty()) mission_queue_.pop(); while (!mission_queue_.empty()) mission_queue_.pop();
if(state_ == MissionState::EMERGENCY) return;
state_ = MissionState::CANCELLED; state_ = MissionState::CANCELLED;
} }
@@ -242,10 +259,19 @@ namespace mission_adapters
state_ = MissionState::EMERGENCY; state_ = MissionState::EMERGENCY;
} }
void MissionManager::clearEmergency()
{
std::lock_guard<std::mutex> lock(mutex_);
if(state_ == MissionState::EMERGENCY)
state_ = MissionState::CLEAR_EMERGENCY;
}
void MissionManager::pause() void MissionManager::pause()
{ {
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
if (state_ == MissionState::RUNNING) if (state_ == MissionState::IDLE ||
state_ == MissionState::RUNNING ||
state_ == MissionState::QUEUED)
state_ = MissionState::PAUSED; state_ = MissionState::PAUSED;
} }
@@ -256,7 +282,7 @@ namespace mission_adapters
if (current_mission_) if (current_mission_)
state_ = MissionState::RUNNING; state_ = MissionState::RUNNING;
else if (!mission_queue_.empty()) else if (!current_mission_ && !mission_queue_.empty())
state_ = MissionState::QUEUED; state_ = MissionState::QUEUED;
else else
state_ = MissionState::IDLE; state_ = MissionState::IDLE;
@@ -321,6 +347,7 @@ namespace mission_adapters
case EventType::RESUME: mission_manager_.resume(); break; case EventType::RESUME: mission_manager_.resume(); break;
case EventType::CANCEL: mission_manager_.cancel(); break; case EventType::CANCEL: mission_manager_.cancel(); break;
case EventType::EMERGENCY: mission_manager_.emergency(); break; case EventType::EMERGENCY: mission_manager_.emergency(); break;
case EventType::CLEAR_EMERGENCY: mission_manager_.clearEmergency(); break;
default: default:
robot::log_error("Unknown event type"); robot::log_error("Unknown event type");
break; break;
@@ -376,6 +403,11 @@ namespace mission_adapters
event_bus_.push({EventType::EMERGENCY, PRIORITY_EMERGENCY, {}}); event_bus_.push({EventType::EMERGENCY, PRIORITY_EMERGENCY, {}});
} }
void EventProcessor::clearEmergencyEvent()
{
event_bus_.push({EventType::CLEAR_EMERGENCY, PRIORITY_EMERGENCY, {}});
}
// ───────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────
// MissionExecutor // MissionExecutor
// ───────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────

View File

@@ -23,7 +23,7 @@ class RobotControlTest
} }
public: public:
RobotControlTest() = default; RobotControlTest();
~RobotControlTest(); ~RobotControlTest();
void run(); void run();
@@ -32,6 +32,9 @@ private:
void executeMission(const Mission& mission); void executeMission(const Mission& mission);
}; };
RobotControlTest::RobotControlTest()
{}
RobotControlTest::~RobotControlTest() RobotControlTest::~RobotControlTest()
{ {
event_processor_.stop(); event_processor_.stop();
@@ -49,27 +52,22 @@ void RobotControlTest::run()
executeMission(*mission); executeMission(*mission);
}); });
event_processor_.start();
mission_executor_.start();
while (true) while (true)
{ {
auto feedback = move_base_ptr_->getFeedback(); auto feedback = move_base_ptr_->getFeedback();
auto nav_state = feedback->navigation_state; auto nav_state = feedback->navigation_state;
// FIX #8: navDoneEvent fires only when navigation AND actions are both done. if (nav_state != prev_nav_done_state_ && nav_state == robot::move_base_core::State::SUCCEEDED && areActionsDone())
if (nav_state != prev_nav_state_)
{ {
if (nav_state == robot::move_base_core::State::SUCCEEDED && areActionsDone()) event_processor_.navDoneEvent();
{
event_processor_.navDoneEvent();
}
else if (nav_state == robot::move_base_core::State::ABORTED)
{
event_processor_.navFailedEvent();
}
prev_nav_state_ = nav_state;
} }
else if (nav_state == robot::move_base_core::State::ABORTED)
{
event_processor_.navFailedEvent();
}
prev_nav_state_ = nav_state;
// Example: receive an order (replace condition with your real source) // Example: receive an order (replace condition with your real source)
if (/* new order available */ false) if (/* new order available */ false)

BIN
src/test_mission_adapters Executable file

Binary file not shown.

578
src/test_mission_adapters.cpp Executable file
View File

@@ -0,0 +1,578 @@
#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <thread>
#include "mission_adapters/mission_adapters.h"
using namespace mission_adapters;
namespace
{
robot_geometry_msgs::PoseStamped MakeGoal(double x, double y)
{
robot_geometry_msgs::PoseStamped g;
g.pose.position.x = x;
g.pose.position.y = y;
return g;
}
robot_protocol_msgs::Action MakeAction(
const std::string &id)
{
robot_protocol_msgs::Action a;
a.actionId = id;
a.actionType = "TEST";
return a;
}
robot_protocol_msgs::Node MakeNode(
int seq,
bool add_action = false)
{
robot_protocol_msgs::Node n;
n.sequenceId = seq;
n.nodeId = "node_" + std::to_string(seq);
n.nodePosition.x = seq;
n.nodePosition.y = seq;
if (add_action)
n.actions.push_back(
MakeAction("node_action_" + std::to_string(seq)));
return n;
}
robot_protocol_msgs::Edge MakeEdge(
int seq,
bool add_action = false)
{
robot_protocol_msgs::Edge e;
e.sequenceId = seq;
e.edgeId = "edge_" + std::to_string(seq);
if (add_action)
e.actions.push_back(
MakeAction("edge_action_" + std::to_string(seq)));
return e;
}
robot_protocol_msgs::Order MakeOrder(
int node_count)
{
robot_protocol_msgs::Order order;
for (int i = 0; i < node_count; ++i)
order.nodes.push_back(
MakeNode(i));
for (int i = 0; i < node_count - 1; ++i)
order.edges.push_back(
MakeEdge(i));
return order;
}
} // namespace
//--------------------------------------------------------------
// EventBus
//--------------------------------------------------------------
TEST(EventBus, PriorityOrdering)
{
EventBus bus;
bus.push({EventType::PAUSE,
PRIORITY_PAUSE,
{}});
bus.push({EventType::CANCEL,
PRIORITY_CANCEL,
{}});
bus.push({EventType::EMERGENCY,
PRIORITY_EMERGENCY,
{}});
Event e;
ASSERT_TRUE(bus.pop(e));
EXPECT_EQ(e.priority,
PRIORITY_EMERGENCY);
ASSERT_TRUE(bus.pop(e));
EXPECT_EQ(e.priority,
PRIORITY_CANCEL);
ASSERT_TRUE(bus.pop(e));
EXPECT_EQ(e.priority,
PRIORITY_PAUSE);
}
TEST(EventBus, StopReturnsFalse)
{
EventBus bus;
std::thread t([&]()
{ bus.stop(); });
Event e;
EXPECT_FALSE(bus.pop(e));
t.join();
}
TEST(EventBus, ConcurrentPush)
{
EventBus bus;
constexpr int N = 100;
std::thread producer([&]()
{
for (int i = 0; i < N; i++)
{
bus.push(
{EventType::NAV_DONE,
PRIORITY_NAV_DONE,
{}});
}
});
int count = 0;
while (count < N)
{
Event e;
if (bus.pop(e))
++count;
}
producer.join();
EXPECT_EQ(count, N);
}
//--------------------------------------------------------------
// GoalAdapter
//--------------------------------------------------------------
TEST(GoalAdapter, ConvertSingleGoal)
{
GoalAdapter adapter;
auto missions =
adapter.convert(
MakeGoal(5.5, 9.1));
ASSERT_EQ(missions.size(), 1u);
EXPECT_EQ(
missions[0]->type,
MissionType::SIMPLE_GOAL);
EXPECT_DOUBLE_EQ(
missions[0]->goal.pose.position.x,
5.5);
EXPECT_DOUBLE_EQ(
missions[0]->goal.pose.position.y,
9.1);
}
//--------------------------------------------------------------
// VDA5050Adapter
//--------------------------------------------------------------
TEST(VDA5050Adapter, EmptyOrder)
{
VDA5050Adapter adapter;
robot_protocol_msgs::Order order;
auto missions =
adapter.convert(order);
EXPECT_TRUE(
missions.empty());
}
TEST(VDA5050Adapter, OrderWithoutActionsCreatesTailMission)
{
VDA5050Adapter adapter;
auto order =
MakeOrder(4);
auto missions =
adapter.convert(order);
ASSERT_EQ(
missions.size(),
1u);
EXPECT_EQ(
missions[0]->nodes.size(),
4u);
EXPECT_EQ(
missions[0]->edges.size(),
3u);
}
TEST(VDA5050Adapter, SplitAtNodeAction)
{
VDA5050Adapter adapter;
auto order =
MakeOrder(5);
order.nodes[2].actions.push_back(
MakeAction("dock"));
auto missions =
adapter.convert(order);
ASSERT_EQ(
missions.size(),
2u);
EXPECT_EQ(
missions[0]->nodes.size(),
3u);
EXPECT_EQ(
missions[1]->nodes.size(),
3u);
}
TEST(VDA5050Adapter, CollectEdgeAction)
{
VDA5050Adapter adapter;
auto order =
MakeOrder(2);
order.nodes[1].actions.push_back(
MakeAction("node"));
order.edges[0].actions.push_back(
MakeAction("edge"));
auto missions =
adapter.convert(order);
ASSERT_EQ(
missions.size(),
1u);
ASSERT_EQ(
missions[0]->actions.size(),
2u);
EXPECT_LT(
missions[0]->actions[0].sequenceId,
missions[0]->actions[1].sequenceId);
}
//--------------------------------------------------------------
// MissionManager
//--------------------------------------------------------------
TEST(MissionManager, SubmitChangesState)
{
MissionManager mgr;
GoalAdapter adapter;
mgr.submit(
adapter.convert(
MakeGoal(1, 2)));
EXPECT_EQ(
mgr.state(),
MissionState::QUEUED);
EXPECT_TRUE(
mgr.hasMission());
}
TEST(MissionManager, NextMission)
{
MissionManager mgr;
GoalAdapter adapter;
mgr.submit(
adapter.convert(
MakeGoal(1, 2)));
auto m =
mgr.nextMission();
ASSERT_NE(
m,
nullptr);
EXPECT_EQ(
mgr.state(),
MissionState::RUNNING);
}
TEST(MissionManager, NavigationDone)
{
MissionManager mgr;
GoalAdapter adapter;
mgr.submit(
adapter.convert(
MakeGoal(0, 0)));
mgr.nextMission();
mgr.onNavigationDone();
EXPECT_EQ(
mgr.state(),
MissionState::IDLE);
EXPECT_FALSE(
mgr.hasMission());
}
TEST(MissionManager, NavigationFailed)
{
MissionManager mgr;
GoalAdapter adapter;
mgr.submit(
adapter.convert(
MakeGoal(0, 0)));
mgr.nextMission();
mgr.onNavigationFailed();
EXPECT_EQ(
mgr.state(),
MissionState::FAILED);
}
TEST(MissionManager, PauseResume)
{
MissionManager mgr;
GoalAdapter adapter;
mgr.submit(
adapter.convert(
MakeGoal(1, 1)));
mgr.pause();
EXPECT_EQ(
mgr.state(),
MissionState::PAUSED);
mgr.resume();
EXPECT_EQ(
mgr.state(),
MissionState::QUEUED);
}
TEST(MissionManager, Cancel)
{
MissionManager mgr;
GoalAdapter adapter;
mgr.submit(
adapter.convert(
MakeGoal(1, 1)));
mgr.cancel();
EXPECT_EQ(
mgr.state(),
MissionState::CANCELLED);
EXPECT_FALSE(
mgr.hasMission());
}
TEST(MissionManager, Emergency)
{
MissionManager mgr;
GoalAdapter adapter;
mgr.submit(
adapter.convert(
MakeGoal(2, 2)));
mgr.emergency();
EXPECT_EQ(
mgr.state(),
MissionState::EMERGENCY);
}
TEST(MissionManager, ClearEmergency)
{
MissionManager mgr;
mgr.emergency();
mgr.clearEmergency();
EXPECT_EQ(
mgr.state(),
MissionState::CLEAR_EMERGENCY);
}
//--------------------------------------------------------------
// EventProcessor
//--------------------------------------------------------------
TEST(EventProcessor, GoalEvent)
{
MissionManager mgr;
EventProcessor proc(mgr);
proc.start();
proc.goalEvent(
MakeGoal(5, 6));
std::this_thread::sleep_for(
std::chrono::milliseconds(100));
EXPECT_EQ(
mgr.state(),
MissionState::QUEUED);
proc.stop();
}
TEST(EventProcessor, EmergencyHasHighestPriority)
{
MissionManager mgr;
EventProcessor proc(mgr);
proc.start();
proc.goalEvent(
MakeGoal(1, 1));
proc.emergencyEvent();
std::this_thread::sleep_for(
std::chrono::milliseconds(100));
EXPECT_EQ(
mgr.state(),
MissionState::EMERGENCY);
proc.stop();
}
//--------------------------------------------------------------
// MissionExecutor
//--------------------------------------------------------------
TEST(MissionExecutor, CallbackInvokedOnce)
{
MissionManager mgr;
GoalAdapter adapter;
mgr.submit(
adapter.convert(
MakeGoal(10, 10)));
MissionExecutor exec(mgr);
std::atomic<int> count{0};
exec.setMissionCallback(
[&](const std::shared_ptr<Mission> &)
{
count++;
});
exec.start();
std::this_thread::sleep_for(
std::chrono::milliseconds(200));
exec.stop();
EXPECT_EQ(
count.load(),
1);
}
TEST(MissionExecutor, NewMissionAfterDone)
{
MissionManager mgr;
GoalAdapter adapter;
mgr.submit(
adapter.convert(
MakeGoal(1, 1)));
MissionExecutor exec(mgr);
std::atomic<int> count{0};
exec.setMissionCallback(
[&](const std::shared_ptr<Mission> &)
{
count++;
});
exec.start();
std::this_thread::sleep_for(
std::chrono::milliseconds(100));
mgr.onNavigationDone();
mgr.submit(
adapter.convert(
MakeGoal(2, 2)));
std::this_thread::sleep_for(
std::chrono::milliseconds(150));
exec.stop();
EXPECT_EQ(
count.load(),
2);
}
//--------------------------------------------------------------
int main(int argc, char **argv)
{
::testing::InitGoogleTest(
&argc,
argv);
return RUN_ALL_TESTS();
}