diff --git a/src/base_autonomy/local_planner/config/unitree/unitree_g1.yaml b/src/base_autonomy/local_planner/config/unitree/unitree_g1.yaml
index 65e70665..65fc6559 100644
--- a/src/base_autonomy/local_planner/config/unitree/unitree_g1.yaml
+++ b/src/base_autonomy/local_planner/config/unitree/unitree_g1.yaml
@@ -26,10 +26,6 @@ localPlanner:
# obstacle height threshold
obstacleHeightThre: 0.1 # 0.1 for flat terrain
- # Sensor offset
- sensorOffsetX: 0.0
- sensorOffsetY: 0.0
-
# Vehicle dimensions
vehicleLength: 0.5
vehicleWidth: 0.5
diff --git a/src/base_autonomy/local_planner/launch/local_planner.launch b/src/base_autonomy/local_planner/launch/local_planner.launch
index 4ef035ff..ff22080d 100644
--- a/src/base_autonomy/local_planner/launch/local_planner.launch
+++ b/src/base_autonomy/local_planner/launch/local_planner.launch
@@ -78,8 +78,8 @@
-
+
-
+
diff --git a/src/base_autonomy/local_planner/launch/local_planner.launch.py b/src/base_autonomy/local_planner/launch/local_planner.launch.py
index abbf5154..4d52e70c 100644
--- a/src/base_autonomy/local_planner/launch/local_planner.launch.py
+++ b/src/base_autonomy/local_planner/launch/local_planner.launch.py
@@ -185,6 +185,21 @@ def generate_launch_description():
]
)
+ # Odom transformer: re-expresses /state_estimation (sensor frame) as
+ # odom_base_link (base_link frame) using the sensor->base_link static TF.
+ odomTransformer_node = Node(
+ package='odom_transformer',
+ executable='odom_transformer_node',
+ name='odom_transformer',
+ output='screen',
+ parameters=[{
+ 'source_frame': 'sensor',
+ 'target_frame': 'base_link',
+ 'odom_topic_in': '/state_estimation',
+ 'odom_topic_out': '/odom_base_link',
+ }]
+ )
+
# Static transform publishers with sensor offsets from config
vehicleTransPublisher_node = Node(
package='tf2_ros',
@@ -195,8 +210,8 @@ def generate_launch_description():
str(-sensor_offsets['sensorOffsetY']),
str(-sensor_offsets['sensorOffsetZ']),
'0', '0', '0',
- '/sensor',
- '/vehicle'
+ 'sensor',
+ 'base_link'
]
)
@@ -211,7 +226,7 @@ def generate_launch_description():
str(camera_offsets['cameraOffsetRoll']),
str(camera_offsets['cameraOffsetPitch']),
str(camera_offsets['cameraOffsetYaw']),
- '/sensor', camera_link
+ 'sensor', camera_link
]
)
@@ -227,4 +242,5 @@ def generate_launch_description():
pathFollower_node,
vehicleTransPublisher_node,
sensorTransPublisher_node,
+ odomTransformer_node,
])
\ No newline at end of file
diff --git a/src/base_autonomy/local_planner/src/localPlanner.cpp b/src/base_autonomy/local_planner/src/localPlanner.cpp
index f41d33a3..2b3d7146 100644
--- a/src/base_autonomy/local_planner/src/localPlanner.cpp
+++ b/src/base_autonomy/local_planner/src/localPlanner.cpp
@@ -52,8 +52,6 @@ double normalizeAngle(double angle) {
string pathFolder;
double vehicleLength = 0.6;
double vehicleWidth = 0.6;
-double sensorOffsetX = 0;
-double sensorOffsetY = 0;
bool twoWayDrive = true;
double laserVoxelSize = 0.05;
double terrainVoxelSize = 0.2;
@@ -169,8 +167,8 @@ void odometryHandler(const nav_msgs::msg::Odometry::ConstSharedPtr odom)
vehicleRoll = roll;
vehiclePitch = pitch;
vehicleYaw = yaw;
- vehicleX = odom->pose.pose.position.x - cos(yaw) * sensorOffsetX + sin(yaw) * sensorOffsetY;
- vehicleY = odom->pose.pose.position.y - sin(yaw) * sensorOffsetX - cos(yaw) * sensorOffsetY;
+ vehicleX = odom->pose.pose.position.x;
+ vehicleY = odom->pose.pose.position.y;
vehicleZ = odom->pose.pose.position.z;
}
@@ -562,8 +560,6 @@ int main(int argc, char** argv)
nh->declare_parameter("pathFolder", pathFolder);
nh->declare_parameter("vehicleLength", vehicleLength);
nh->declare_parameter("vehicleWidth", vehicleWidth);
- nh->declare_parameter("sensorOffsetX", sensorOffsetX);
- nh->declare_parameter("sensorOffsetY", sensorOffsetY);
nh->declare_parameter("twoWayDrive", twoWayDrive);
nh->declare_parameter("laserVoxelSize", laserVoxelSize);
nh->declare_parameter("terrainVoxelSize", terrainVoxelSize);
@@ -610,8 +606,6 @@ int main(int argc, char** argv)
nh->get_parameter("pathFolder", pathFolder);
nh->get_parameter("vehicleLength", vehicleLength);
nh->get_parameter("vehicleWidth", vehicleWidth);
- nh->get_parameter("sensorOffsetX", sensorOffsetX);
- nh->get_parameter("sensorOffsetY", sensorOffsetY);
nh->get_parameter("twoWayDrive", twoWayDrive);
nh->get_parameter("laserVoxelSize", laserVoxelSize);
nh->get_parameter("terrainVoxelSize", terrainVoxelSize);
@@ -655,7 +649,7 @@ int main(int argc, char** argv)
nh->get_parameter("goalX", goalX);
nh->get_parameter("goalY", goalY);
- auto subOdometry = nh->create_subscription("/state_estimation", 5, odometryHandler);
+ auto subOdometry = nh->create_subscription("/odom_base_link", 5, odometryHandler);
auto subLaserCloud = nh->create_subscription("/registered_scan", 5, laserCloudHandler);
@@ -1061,7 +1055,7 @@ int main(int argc, char** argv)
}
path.header.stamp = rclcpp::Time(static_cast(odomTime * 1e9));
- path.header.frame_id = "vehicle";
+ path.header.frame_id = "base_link";
pubPath->publish(path);
#if PLOTPATHSET == 1
@@ -1107,7 +1101,7 @@ int main(int argc, char** argv)
sensor_msgs::msg::PointCloud2 freePaths2;
pcl::toROSMsg(*freePaths, freePaths2);
freePaths2.header.stamp = rclcpp::Time(static_cast(odomTime * 1e9));
- freePaths2.header.frame_id = "vehicle";
+ freePaths2.header.frame_id = "base_link";
pubFreePaths->publish(freePaths2);
#endif
}
@@ -1137,7 +1131,7 @@ int main(int argc, char** argv)
path.poses[0].pose.orientation.w = 0;
path.header.stamp = rclcpp::Time(static_cast(odomTime * 1e9));
- path.header.frame_id = "vehicle";
+ path.header.frame_id = "base_link";
pubPath->publish(path);
#if PLOTPATHSET == 1
@@ -1145,7 +1139,7 @@ int main(int argc, char** argv)
sensor_msgs::msg::PointCloud2 freePaths2;
pcl::toROSMsg(*freePaths, freePaths2);
freePaths2.header.stamp = rclcpp::Time(static_cast(odomTime * 1e9));
- freePaths2.header.frame_id = "vehicle";
+ freePaths2.header.frame_id = "base_link";
pubFreePaths->publish(freePaths2);
#endif
}
@@ -1153,7 +1147,7 @@ int main(int argc, char** argv)
/*sensor_msgs::msg::PointCloud2 plannerCloud2;
pcl::toROSMsg(*plannerCloudCrop, plannerCloud2);
plannerCloud2.header.stamp = rclcpp::Time(static_cast(odomTime * 1e9));
- plannerCloud2.header.frame_id = "vehicle";
+ plannerCloud2.header.frame_id = "base_link";
pubLaserCloud->publish(plannerCloud2);*/
}
diff --git a/src/base_autonomy/local_planner/src/pathFollower.cpp b/src/base_autonomy/local_planner/src/pathFollower.cpp
index 6805f113..29e6be3f 100644
--- a/src/base_autonomy/local_planner/src/pathFollower.cpp
+++ b/src/base_autonomy/local_planner/src/pathFollower.cpp
@@ -46,8 +46,6 @@ double normalizeAngle(double angle) {
bool realRobot = false;
string serialPort = "/dev/ttyACM0";
int baudrate = 115200;
-double sensorOffsetX = 0;
-double sensorOffsetY = 0;
int pubSkipNum = 1;
int pubSkipCount = 0;
bool twoWayDrive = true;
@@ -133,8 +131,8 @@ void odomHandler(const nav_msgs::msg::Odometry::ConstSharedPtr odomIn)
vehicleRoll = roll;
vehiclePitch = pitch;
vehicleYaw = yaw;
- vehicleX = odomIn->pose.pose.position.x - cos(yaw) * sensorOffsetX + sin(yaw) * sensorOffsetY;
- vehicleY = odomIn->pose.pose.position.y - sin(yaw) * sensorOffsetX - cos(yaw) * sensorOffsetY;
+ vehicleX = odomIn->pose.pose.position.x;
+ vehicleY = odomIn->pose.pose.position.y;
vehicleZ = odomIn->pose.pose.position.z;
if ((fabs(roll) > inclThre * PI / 180.0 || fabs(pitch) > inclThre * PI / 180.0) && useInclToStop) {
@@ -246,8 +244,6 @@ int main(int argc, char** argv)
nh->declare_parameter("realRobot", realRobot);
nh->declare_parameter("serialPort", serialPort);
nh->declare_parameter("baudrate", baudrate);
- nh->declare_parameter("sensorOffsetX", sensorOffsetX);
- nh->declare_parameter("sensorOffsetY", sensorOffsetY);
nh->declare_parameter("pubSkipNum", pubSkipNum);
nh->declare_parameter("twoWayDrive", twoWayDrive);
nh->declare_parameter("lookAheadDis", lookAheadDis);
@@ -282,8 +278,6 @@ int main(int argc, char** argv)
nh->get_parameter("realRobot", realRobot);
nh->get_parameter("serialPort", serialPort);
nh->get_parameter("baudrate", baudrate);
- nh->get_parameter("sensorOffsetX", sensorOffsetX);
- nh->get_parameter("sensorOffsetY", sensorOffsetY);
nh->get_parameter("pubSkipNum", pubSkipNum);
nh->get_parameter("twoWayDrive", twoWayDrive);
nh->get_parameter("lookAheadDis", lookAheadDis);
@@ -315,7 +309,7 @@ int main(int argc, char** argv)
nh->get_parameter("joyToSpeedDelay", joyToSpeedDelay);
nh->get_parameter("goalYawGain", goalYawGain);
- auto subOdom = nh->create_subscription("/state_estimation", 5, odomHandler);
+ auto subOdom = nh->create_subscription("/odom_base_link", 5, odomHandler);
auto subPath = nh->create_subscription("/path", 5, pathHandler);
@@ -327,9 +321,9 @@ int main(int argc, char** argv)
auto subSlowDown = nh->create_subscription("/slow_down", 5, slowDownHandler);
- auto pubSpeed = nh->create_publisher("/cmd_vel", 5);
+ auto pubSpeed = nh->create_publisher("/navigation_cmd_vel", 5);
geometry_msgs::msg::TwistStamped cmd_vel;
- cmd_vel.header.frame_id = "vehicle";
+ cmd_vel.header.frame_id = "base_link";
if (autonomyMode) {
joySpeed = autonomySpeed / maxSpeed;
diff --git a/src/base_autonomy/vehicle_simulator/launch/system_bagfile.launch.py b/src/base_autonomy/vehicle_simulator/launch/system_bagfile.launch.py
index fc78986c..4c96c032 100644
--- a/src/base_autonomy/vehicle_simulator/launch/system_bagfile.launch.py
+++ b/src/base_autonomy/vehicle_simulator/launch/system_bagfile.launch.py
@@ -86,6 +86,12 @@ def generate_launch_description():
}]
)
+ start_cmd_vel_mux = IncludeLaunchDescription(
+ PythonLaunchDescriptionSource(os.path.join(
+ get_package_share_directory('cmd_vel_mux'), 'launch', 'cmd_vel_mux-launch.py')
+ )
+ )
+
ld = LaunchDescription()
# Add the actions
@@ -105,4 +111,6 @@ def generate_launch_description():
ld.add_action(start_visualization_tools)
ld.add_action(start_joy)
+ ld.add_action(start_cmd_vel_mux)
+
return ld
diff --git a/src/base_autonomy/vehicle_simulator/launch/system_bagfile_with_exploration_planner.launch.py b/src/base_autonomy/vehicle_simulator/launch/system_bagfile_with_exploration_planner.launch.py
index 9f8328aa..b558a940 100644
--- a/src/base_autonomy/vehicle_simulator/launch/system_bagfile_with_exploration_planner.launch.py
+++ b/src/base_autonomy/vehicle_simulator/launch/system_bagfile_with_exploration_planner.launch.py
@@ -96,6 +96,12 @@ def generate_launch_description():
}.items()
)
+ start_cmd_vel_mux = IncludeLaunchDescription(
+ PythonLaunchDescriptionSource(os.path.join(
+ get_package_share_directory('cmd_vel_mux'), 'launch', 'cmd_vel_mux-launch.py')
+ )
+ )
+
ld = LaunchDescription()
# Add the actions
@@ -117,4 +123,6 @@ def generate_launch_description():
ld.add_action(start_joy)
ld.add_action(start_tare_planner)
+ ld.add_action(start_cmd_vel_mux)
+
return ld
diff --git a/src/base_autonomy/vehicle_simulator/launch/system_bagfile_with_route_planner.launch.py b/src/base_autonomy/vehicle_simulator/launch/system_bagfile_with_route_planner.launch.py
index 1c3d3d15..a521e74e 100644
--- a/src/base_autonomy/vehicle_simulator/launch/system_bagfile_with_route_planner.launch.py
+++ b/src/base_autonomy/vehicle_simulator/launch/system_bagfile_with_route_planner.launch.py
@@ -114,6 +114,12 @@ def generate_launch_description():
condition=IfCondition(use_pct_planner)
)
+ start_cmd_vel_mux = IncludeLaunchDescription(
+ PythonLaunchDescriptionSource(os.path.join(
+ get_package_share_directory('cmd_vel_mux'), 'launch', 'cmd_vel_mux-launch.py')
+ )
+ )
+
ld = LaunchDescription()
# Add the actions
@@ -137,4 +143,6 @@ def generate_launch_description():
ld.add_action(start_far_planner)
ld.add_action(start_pct_planner)
+ ld.add_action(start_cmd_vel_mux)
+
return ld
diff --git a/src/base_autonomy/vehicle_simulator/launch/system_real_robot.launch.py b/src/base_autonomy/vehicle_simulator/launch/system_real_robot.launch.py
index 38fbf22c..40739917 100644
--- a/src/base_autonomy/vehicle_simulator/launch/system_real_robot.launch.py
+++ b/src/base_autonomy/vehicle_simulator/launch/system_real_robot.launch.py
@@ -91,6 +91,12 @@ def generate_launch_description():
[get_package_share_directory('livox_ros_driver2'), '/launch_ROS2/msg_MID360_launch.py']),
)
+ start_cmd_vel_mux = IncludeLaunchDescription(
+ PythonLaunchDescriptionSource(os.path.join(
+ get_package_share_directory('cmd_vel_mux'), 'launch', 'cmd_vel_mux-launch.py')
+ )
+ )
+
ld = LaunchDescription()
# Add the actions
@@ -111,4 +117,6 @@ def generate_launch_description():
ld.add_action(start_joy)
ld.add_action(start_mid360)
+ ld.add_action(start_cmd_vel_mux)
+
return ld
diff --git a/src/base_autonomy/vehicle_simulator/launch/system_real_robot_with_exploration_planner.launch.py b/src/base_autonomy/vehicle_simulator/launch/system_real_robot_with_exploration_planner.launch.py
index 0a554c86..94b863f3 100644
--- a/src/base_autonomy/vehicle_simulator/launch/system_real_robot_with_exploration_planner.launch.py
+++ b/src/base_autonomy/vehicle_simulator/launch/system_real_robot_with_exploration_planner.launch.py
@@ -101,6 +101,12 @@ def generate_launch_description():
[get_package_share_directory('livox_ros_driver2'), '/launch_ROS2/msg_MID360_launch.py']),
)
+ start_cmd_vel_mux = IncludeLaunchDescription(
+ PythonLaunchDescriptionSource(os.path.join(
+ get_package_share_directory('cmd_vel_mux'), 'launch', 'cmd_vel_mux-launch.py')
+ )
+ )
+
ld = LaunchDescription()
# Add the actions
@@ -123,4 +129,6 @@ def generate_launch_description():
ld.add_action(start_tare_planner)
ld.add_action(start_mid360)
+ ld.add_action(start_cmd_vel_mux)
+
return ld
diff --git a/src/base_autonomy/vehicle_simulator/launch/system_real_robot_with_route_planner.launch.py b/src/base_autonomy/vehicle_simulator/launch/system_real_robot_with_route_planner.launch.py
index bf394c7a..c483113a 100644
--- a/src/base_autonomy/vehicle_simulator/launch/system_real_robot_with_route_planner.launch.py
+++ b/src/base_autonomy/vehicle_simulator/launch/system_real_robot_with_route_planner.launch.py
@@ -119,6 +119,12 @@ def generate_launch_description():
[get_package_share_directory('livox_ros_driver2'), '/launch_ROS2/msg_MID360_launch.py']),
)
+ start_cmd_vel_mux = IncludeLaunchDescription(
+ PythonLaunchDescriptionSource(os.path.join(
+ get_package_share_directory('cmd_vel_mux'), 'launch', 'cmd_vel_mux-launch.py')
+ )
+ )
+
ld = LaunchDescription()
# Add the actions
@@ -143,4 +149,6 @@ def generate_launch_description():
ld.add_action(start_pct_planner)
ld.add_action(start_mid360)
+ ld.add_action(start_cmd_vel_mux)
+
return ld
diff --git a/src/base_autonomy/vehicle_simulator/launch/system_simulation.launch.py b/src/base_autonomy/vehicle_simulator/launch/system_simulation.launch.py
index 7da08415..3b8f2245 100644
--- a/src/base_autonomy/vehicle_simulator/launch/system_simulation.launch.py
+++ b/src/base_autonomy/vehicle_simulator/launch/system_simulation.launch.py
@@ -119,6 +119,12 @@ def generate_launch_description():
}]
)
+ start_cmd_vel_mux = IncludeLaunchDescription(
+ PythonLaunchDescriptionSource(os.path.join(
+ get_package_share_directory('cmd_vel_mux'), 'launch', 'cmd_vel_mux-launch.py')
+ )
+ )
+
ld = LaunchDescription()
# Add the actions
@@ -141,4 +147,6 @@ def generate_launch_description():
ld.add_action(start_visualization_tools)
ld.add_action(start_joy)
+ ld.add_action(start_cmd_vel_mux)
+
return ld
diff --git a/src/base_autonomy/vehicle_simulator/launch/system_simulation_with_exploration_planner.launch.py b/src/base_autonomy/vehicle_simulator/launch/system_simulation_with_exploration_planner.launch.py
index 6725f501..4800757a 100644
--- a/src/base_autonomy/vehicle_simulator/launch/system_simulation_with_exploration_planner.launch.py
+++ b/src/base_autonomy/vehicle_simulator/launch/system_simulation_with_exploration_planner.launch.py
@@ -129,6 +129,12 @@ def generate_launch_description():
}.items()
)
+ start_cmd_vel_mux = IncludeLaunchDescription(
+ PythonLaunchDescriptionSource(os.path.join(
+ get_package_share_directory('cmd_vel_mux'), 'launch', 'cmd_vel_mux-launch.py')
+ )
+ )
+
ld = LaunchDescription()
# Add the actions
@@ -153,4 +159,6 @@ def generate_launch_description():
ld.add_action(start_joy)
ld.add_action(start_tare_planner)
+ ld.add_action(start_cmd_vel_mux)
+
return ld
diff --git a/src/base_autonomy/vehicle_simulator/package.xml b/src/base_autonomy/vehicle_simulator/package.xml
index 6fbb4732..6733ff1f 100644
--- a/src/base_autonomy/vehicle_simulator/package.xml
+++ b/src/base_autonomy/vehicle_simulator/package.xml
@@ -20,6 +20,8 @@
ros_tcp_endpoint
cv_bridge
+ cmd_vel_mux
+
ament_lint_auto
ament_lint_common
diff --git a/src/base_autonomy/vehicle_simulator/rviz/vehicle_simulator.rviz b/src/base_autonomy/vehicle_simulator/rviz/vehicle_simulator.rviz
index d3341823..2dbe581a 100644
--- a/src/base_autonomy/vehicle_simulator/rviz/vehicle_simulator.rviz
+++ b/src/base_autonomy/vehicle_simulator/rviz/vehicle_simulator.rviz
@@ -33,7 +33,7 @@ Visualization Manager:
Length: 1.5
Name: Vehicle
Radius: 0.20000000298023224
- Reference Frame: vehicle
+ Reference Frame: base_link
Value: true
- Class: rviz_default_plugins/Image
Enabled: true
diff --git a/src/base_autonomy/visualization_tools/launch/visualization_tools.launch b/src/base_autonomy/visualization_tools/launch/visualization_tools.launch
index 1fc04423..7e9686b1 100755
--- a/src/base_autonomy/visualization_tools/launch/visualization_tools.launch
+++ b/src/base_autonomy/visualization_tools/launch/visualization_tools.launch
@@ -3,10 +3,10 @@
-
-
-
-
+
+
+
+
diff --git a/src/base_autonomy/visualization_tools/src/visualizationTools.cpp b/src/base_autonomy/visualization_tools/src/visualizationTools.cpp
index 68a5b59b..9c37ab46 100644
--- a/src/base_autonomy/visualization_tools/src/visualizationTools.cpp
+++ b/src/base_autonomy/visualization_tools/src/visualizationTools.cpp
@@ -404,11 +404,6 @@ int main(int argc, char** argv)
nh->get_parameter("saveTraj", saveTraj);
nh->get_parameter("savePcd", savePcd);
- // No direct replacement present for $(find pkg) in ROS2. Edit file path.
- mapFile.replace(mapFile.find("/install/"), 8, "/src/base_autonomy");
- metricFile.replace(metricFile.find("/install/"), 8, "/src/base_autonomy");
- trajFile.replace(trajFile.find("/install/"), 8, "/src/base_autonomy");
- pcdFile.replace(pcdFile.find("/install/"), 8, "/src/base_autonomy");
auto subOdometry = nh->create_subscription("/state_estimation", 5, odometryHandler);
diff --git a/src/route_planner/far_planner/launch/far_planner.launch b/src/route_planner/far_planner/launch/far_planner.launch
index d33606c0..77db6d60 100644
--- a/src/route_planner/far_planner/launch/far_planner.launch
+++ b/src/route_planner/far_planner/launch/far_planner.launch
@@ -27,7 +27,7 @@ def generate_launch_description():
'.yaml"'])
],
remappings=[
- ('/odom_world', '/state_estimation'),
+ ('/odom_world', '/odom_base_link'),
('/terrain_cloud', '/terrain_map_ext'),
('/scan_cloud', '/terrain_map'),
('/terrain_local_cloud', '/registered_scan')
diff --git a/src/utilities/cmd_vel_mux/CMakeLists.txt b/src/utilities/cmd_vel_mux/CMakeLists.txt
new file mode 100644
index 00000000..a426a08e
--- /dev/null
+++ b/src/utilities/cmd_vel_mux/CMakeLists.txt
@@ -0,0 +1,64 @@
+cmake_minimum_required(VERSION 3.5)
+project(cmd_vel_mux)
+
+if(NOT CMAKE_CXX_STANDARD)
+ set(CMAKE_CXX_STANDARD 14)
+endif()
+
+if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
+ add_compile_options(-Wall -Wextra -Wpedantic)
+endif()
+
+find_package(ament_cmake REQUIRED)
+find_package(geometry_msgs REQUIRED)
+find_package(rcl_interfaces REQUIRED)
+find_package(rclcpp REQUIRED)
+find_package(rclcpp_components REQUIRED)
+find_package(rcpputils REQUIRED)
+find_package(std_msgs REQUIRED)
+
+add_library(${PROJECT_NAME} SHARED src/cmd_vel_mux.cpp)
+target_include_directories(${PROJECT_NAME} PUBLIC
+ $
+ $
+)
+ament_target_dependencies(${PROJECT_NAME}
+ geometry_msgs
+ rcl_interfaces
+ rclcpp
+ rclcpp_components
+ rcpputils
+ std_msgs
+)
+
+add_executable(cmd_vel_mux_node src/cmd_vel_mux_node.cpp)
+target_link_libraries(cmd_vel_mux_node ${PROJECT_NAME})
+ament_target_dependencies(cmd_vel_mux_node rclcpp)
+
+rclcpp_components_register_nodes(${PROJECT_NAME}
+ "cmd_vel_mux::CmdVelMux")
+
+install(TARGETS ${PROJECT_NAME}
+ ARCHIVE DESTINATION lib
+ LIBRARY DESTINATION lib
+ RUNTIME DESTINATION bin
+)
+
+install(TARGETS cmd_vel_mux_node
+ DESTINATION lib/${PROJECT_NAME}
+)
+
+install(DIRECTORY include/${PROJECT_NAME}/
+ DESTINATION include/${PROJECT_NAME}
+)
+
+install(DIRECTORY config launch
+ DESTINATION share/${PROJECT_NAME}
+)
+
+if(BUILD_TESTING)
+ find_package(ament_lint_auto REQUIRED)
+ ament_lint_auto_find_test_dependencies()
+endif()
+
+ament_package()
diff --git a/src/utilities/cmd_vel_mux/README.md b/src/utilities/cmd_vel_mux/README.md
new file mode 100644
index 00000000..e7f8ed17
--- /dev/null
+++ b/src/utilities/cmd_vel_mux/README.md
@@ -0,0 +1,33 @@
+# cmd_vel_mux
+
+Velocity command multiplexer for ROS2. Arbitrates incoming velocity commands from multiple topics based on configurable priorities. Supports both `geometry_msgs/msg/Twist` and `geometry_msgs/msg/TwistStamped` per-subscriber and on the output. Includes an emergency stop that immediately zeroes output.
+
+## Topics
+
+**Published:**
+- `cmd_vel` (`TwistStamped` or `Twist`, controlled by `output_stamped` parameter) — Highest-priority velocity command
+- `active` (`std_msgs/msg/String`) — Name of the currently active input, or "idle"
+
+**Subscribed:**
+- `/stop` (`std_msgs/msg/Int8`) — Value >= 1 immediately zeroes output and blocks all input; 0 resumes
+- Per-subscriber input topics configured via parameters, each independently `Twist` or `TwistStamped`
+
+## Parameters
+
+**Node-level:**
+- `output_stamped` (bool, default `true`) — Publish `TwistStamped` (with timestamp and frame "vehicle") or plain `Twist`
+
+**Per-subscriber** under `subscribers.`:
+- `topic` (string) — Input topic name
+- `timeout` (double) — Seconds of silence before input is deactivated
+- `priority` (int) — Higher value = higher priority, must be unique
+- `short_desc` (string) — Human-readable label
+- `stamped` (bool, default `false`) — Whether the input topic is `TwistStamped` or `Twist`
+
+See `config/cmd_vel_mux_params.yaml` for an example configuration.
+
+## Usage
+
+```bash
+ros2 launch cmd_vel_mux cmd_vel_mux-launch.py
+```
diff --git a/src/utilities/cmd_vel_mux/config/cmd_vel_mux_params.yaml b/src/utilities/cmd_vel_mux/config/cmd_vel_mux_params.yaml
new file mode 100644
index 00000000..7c9107ea
--- /dev/null
+++ b/src/utilities/cmd_vel_mux/config/cmd_vel_mux_params.yaml
@@ -0,0 +1,28 @@
+cmd_vel_mux:
+ ros__parameters:
+ output_stamped: true
+ subscribers:
+ navigation:
+ topic: "/navigation_cmd_vel"
+ timeout: 0.5
+ priority: 3
+ short_desc: "Autonomous navigation stack"
+ stamped: true
+ safety:
+ topic: "/safety_cmd_vel"
+ timeout: 0.1
+ priority: 8
+ short_desc: "Safety controller / obstacle avoidance"
+ stamped: true
+ local_movement:
+ topic: "/local_movement_cmd_vel"
+ timeout: 0.3
+ priority: 5
+ short_desc: "Local precise movements"
+ stamped: true
+ teleop:
+ topic: "/teleop_cmd_vel"
+ timeout: 0.1
+ priority: 10
+ short_desc: "Teleoperation / joystick"
+ stamped: true
\ No newline at end of file
diff --git a/src/utilities/cmd_vel_mux/include/cmd_vel_mux/cmd_vel_mux.hpp b/src/utilities/cmd_vel_mux/include/cmd_vel_mux/cmd_vel_mux.hpp
new file mode 100644
index 00000000..9f483551
--- /dev/null
+++ b/src/utilities/cmd_vel_mux/include/cmd_vel_mux/cmd_vel_mux.hpp
@@ -0,0 +1,97 @@
+#ifndef CMD_VEL_MUX__CMD_VEL_MUX_HPP_
+#define CMD_VEL_MUX__CMD_VEL_MUX_HPP_
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include