Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,27 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Install xlings and project dependencies
- name: Install system and ROS dependencies
run: |
apt-get update && apt-get install -y curl python3-pip git libopencv-dev
apt-get install -y --no-install-recommends \
ros-${{ matrix.ros_distro }}-tf2-ros \
ros-${{ matrix.ros_distro }}-tf2-geometry-msgs \
ros-${{ matrix.ros_distro }}-cv-bridge \
ros-${{ matrix.ros_distro }}-image-transport \
ros-${{ matrix.ros_distro }}-rosbag2-cpp \
ros-${{ matrix.ros_distro }}-rosbag2-transport \
ros-${{ matrix.ros_distro }}-nav2-costmap-2d \
ros-${{ matrix.ros_distro }}-hardware-interface \
ros-${{ matrix.ros_distro }}-robot-state-publisher \
ros-${{ matrix.ros_distro }}-joint-state-publisher \
ros-${{ matrix.ros_distro }}-diagnostic-updater \
ros-${{ matrix.ros_distro }}-example-interfaces

- name: Install xlings and xmake
env:
XLINGS_NON_INTERACTIVE: 1
run: |
apt-get update && apt-get install -y curl python3-pip git
curl -fsSL https://raw.githubusercontent.com/d2learn/xlings/refs/heads/main/tools/other/quick_install.sh | bash
export PATH="$HOME/.xlings/bin:$HOME/.xlings/subos/current/bin:$PATH"
xlings install --verbose
Expand All @@ -34,7 +50,7 @@ jobs:
echo "XMAKE_ROOT=y" >> $GITHUB_ENV

- name: Install colcon-xmake
run: python3 -m pip install --break-system-packages --no-deps git+https://github.com/ros-x/colcon-xmake.git
run: python3 -m pip install --break-system-packages git+https://github.com/ros-x/colcon-xmake.git

- name: Clone ament_xmake
run: |
Expand All @@ -47,14 +63,12 @@ jobs:
source /opt/ros/${{ matrix.ros_distro }}/setup.bash
colcon build --packages-select ament_xmake --event-handlers console_direct+

- name: Build workspace
- name: Build all packages
shell: bash
run: |
source /opt/ros/${{ matrix.ros_distro }}/setup.bash
source install/setup.bash
colcon build \
--packages-up-to downstream_cmake_consumer demo_xmake_launch demo_xmake_custom_msg_node demo_xmake_component demo_xmake_mixed \
--event-handlers console_direct+
colcon build --event-handlers console_direct+

- name: Run tests
shell: bash
Expand Down
53 changes: 46 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,54 @@ Example ROS 2 packages demonstrating [xmake](https://xmake.io/) as a build syste

## Packages

### Level 1: Beginner — ROS 2 Basics

| Package | Description |
|---------|-------------|
| `demo_xmake_pubsub` | Publisher/Subscriber with Topic communication |
| `demo_xmake_service` | Service Client/Server with `example_interfaces` |
| `demo_xmake_timer` | Timers and ROS 2 logging system |

### Level 2: Intermediate — Core Features

| Package | Description |
|---------|-------------|
| `demo_xmake_cpp` | Library build (static/shared) with `add_ros_deps()` and turtlesim |
| `demo_xmake_params` | Parameter system with YAML config and launch files |
| `demo_xmake_interfaces` | Custom msg/srv/action definitions (rosidl) |
| `demo_xmake_component` | Composable node plugin with `install_ros_plugin()` |
| `demo_xmake_action` | Action server/client using custom Countdown action |
| `demo_xmake_lifecycle` | Lifecycle (managed) node with state machine |
| `demo_xmake_launch` | Launch file and config installation |
| `demo_xmake_mixed` | Mixed C++/Python package |
| `demo_xmake_qos` | QoS profiles: reliable, best-effort, sensor data |
| `demo_xmake_testing` | gtest integration with `colcon test` |
| `downstream_cmake_consumer` | CMake interop — consuming xmake package exports |
| `demo_xmake_custom_msg_node` | Node publishing custom messages from `demo_xmake_interfaces` |

### Level 3: Advanced — Robotics Ecosystem

| Package | Description |
|---------|-------------|
| `demo_xmake_tf2` | TF2 coordinate transforms (static + dynamic) |
| `demo_xmake_robot_description` | URDF robot model with robot_state_publisher |
| `demo_xmake_rviz_markers` | RViz Marker/MarkerArray visualization |
| `demo_xmake_executor` | Executors and CallbackGroups (single/multi-threaded) |
| `demo_xmake_image` | Image processing with cv_bridge and OpenCV |
| `demo_xmake_rosbag` | Programmatic rosbag2 recording/playback |
| `demo_xmake_nav_plugin` | Nav2 costmap layer plugin |
| `demo_xmake_hardware` | ros2_control hardware interface plugin |
| `demo_xmake_dds_config` | DDS/RMW configuration (FastDDS/CycloneDDS) |

### Level 4: Debugging & Performance

| Package | Description |
|---------|-------------|
| `demo_xmake_cpp` | Core example using `build_type=ament_xmake` with `add_ros_deps()` for ROS package-oriented linking |
| `demo_xmake_interfaces` | Custom message/service definitions built with xmake (rosidl) |
| `demo_xmake_custom_msg_node` | Node that publishes custom messages from `demo_xmake_interfaces` |
| `demo_xmake_component` | Composable node (rclcpp_components plugin) built with xmake |
| `demo_xmake_launch` | Launch file integration example |
| `demo_xmake_mixed` | Mixed C++/Python package built with xmake |
| `downstream_cmake_consumer` | Standard `ament_cmake` package that consumes exports from `demo_xmake_cpp` |
| `demo_xmake_logging` | Advanced logging and diagnostics with `diagnostic_updater` |
| `demo_xmake_tracing` | Performance tracing with multi-stage pipeline |
| `demo_xmake_benchmark` | Communication latency and throughput benchmarks |

> **Documentation**: See [docs/README.md](docs/README.md) for the full tutorial index (in Chinese) with both ROS 2 learning path and xmake feature views.

## Setup

Expand Down
18 changes: 18 additions & 0 deletions demo_xmake_action/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>demo_xmake_action</name>
<version>0.1.0</version>
<description>Example ROS 2 action server/client built with xmake.</description>
<maintainer email="devnull@example.com">ros2_xmake maintainers</maintainer>
<license>Apache License 2.0</license>

<buildtool_depend>ament_xmake</buildtool_depend>
<depend>rclcpp</depend>
<depend>rclcpp_action</depend>
<depend>demo_xmake_interfaces</depend>

<export>
<build_type>ament_xmake</build_type>
</export>
</package>
71 changes: 71 additions & 0 deletions demo_xmake_action/src/countdown_client.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#include <chrono>
#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "demo_xmake_interfaces/action/countdown.hpp"

using Countdown = demo_xmake_interfaces::action::Countdown;
using GoalHandleCountdown = rclcpp_action::ClientGoalHandle<Countdown>;

class CountdownClient : public rclcpp::Node
{
public:
CountdownClient() : Node("countdown_client")
{
client_ = rclcpp_action::create_client<Countdown>(this, "countdown");
}

void send_goal(int32_t target)
{
using namespace std::chrono_literals;

if (!client_->wait_for_action_server(5s)) {
RCLCPP_ERROR(this->get_logger(), "Action server not available");
return;
}

auto goal = Countdown::Goal();
goal.target_number = target;

auto send_goal_options = rclcpp_action::Client<Countdown>::SendGoalOptions();
send_goal_options.goal_response_callback =
[this](const GoalHandleCountdown::SharedPtr & goal_handle) {
if (!goal_handle) {
RCLCPP_ERROR(this->get_logger(), "Goal rejected");
} else {
RCLCPP_INFO(this->get_logger(), "Goal accepted");
}
};
send_goal_options.feedback_callback =
[this](GoalHandleCountdown::SharedPtr,
const std::shared_ptr<const Countdown::Feedback> feedback) {
RCLCPP_INFO(this->get_logger(), "Feedback: %d", feedback->current_number);
};
send_goal_options.result_callback =
[this](const GoalHandleCountdown::WrappedResult & result) {
if (result.result->success) {
RCLCPP_INFO(this->get_logger(), "Countdown succeeded!");
} else {
RCLCPP_WARN(this->get_logger(), "Countdown did not complete");
}
rclcpp::shutdown();
};

RCLCPP_INFO(this->get_logger(), "Sending goal: count from %d", target);
client_->async_send_goal(goal, send_goal_options);
}

private:
rclcpp_action::Client<Countdown>::SharedPtr client_;
};

int main(int argc, char ** argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<CountdownClient>();
node->send_goal(5);
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
88 changes: 88 additions & 0 deletions demo_xmake_action/src/countdown_server.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#include <chrono>
#include <memory>
#include <thread>

#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "demo_xmake_interfaces/action/countdown.hpp"

using Countdown = demo_xmake_interfaces::action::Countdown;
using GoalHandleCountdown = rclcpp_action::ServerGoalHandle<Countdown>;

class CountdownServer : public rclcpp::Node
{
public:
CountdownServer() : Node("countdown_server")
{
action_server_ = rclcpp_action::create_server<Countdown>(
this, "countdown",
std::bind(&CountdownServer::handle_goal, this,
std::placeholders::_1, std::placeholders::_2),
std::bind(&CountdownServer::handle_cancel, this, std::placeholders::_1),
std::bind(&CountdownServer::handle_accepted, this, std::placeholders::_1));
RCLCPP_INFO(this->get_logger(), "Countdown action server ready");
}

private:
rclcpp_action::GoalResponse handle_goal(
const rclcpp_action::GoalUUID &,
std::shared_ptr<const Countdown::Goal> goal)
{
RCLCPP_INFO(this->get_logger(), "Received goal: count down from %d",
goal->target_number);
if (goal->target_number < 0) {
return rclcpp_action::GoalResponse::REJECT;
}
return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
}

rclcpp_action::CancelResponse handle_cancel(
const std::shared_ptr<GoalHandleCountdown>)
{
RCLCPP_INFO(this->get_logger(), "Cancellation requested");
return rclcpp_action::CancelResponse::ACCEPT;
}

void handle_accepted(const std::shared_ptr<GoalHandleCountdown> goal_handle)
{
std::thread([this, goal_handle]() { execute(goal_handle); }).detach();
}

void execute(const std::shared_ptr<GoalHandleCountdown> goal_handle)
{
RCLCPP_INFO(this->get_logger(), "Executing countdown...");
auto feedback = std::make_shared<Countdown::Feedback>();
auto result = std::make_shared<Countdown::Result>();
int32_t current = goal_handle->get_goal()->target_number;

rclcpp::Rate rate(1.0);
while (current > 0 && rclcpp::ok()) {
if (goal_handle->is_canceling()) {
result->success = false;
goal_handle->canceled(result);
RCLCPP_INFO(this->get_logger(), "Goal canceled at %d", current);
return;
}

feedback->current_number = current;
goal_handle->publish_feedback(feedback);
RCLCPP_INFO(this->get_logger(), "Countdown: %d", current);
--current;
rate.sleep();
}

result->success = true;
goal_handle->succeed(result);
RCLCPP_INFO(this->get_logger(), "Countdown complete!");
}

rclcpp_action::Server<Countdown>::SharedPtr action_server_;
};

int main(int argc, char ** argv)
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<CountdownServer>());
rclcpp::shutdown();
return 0;
}
18 changes: 18 additions & 0 deletions demo_xmake_action/xmake.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
set_project("demo_xmake_action")
set_version("0.1.0")
set_languages("cxx17")

add_rules("mode.debug", "mode.release")
add_rules("ament_xmake.package")

target("countdown_server")
set_kind("binary")
set_installdir("lib/demo_xmake_action")
add_files("src/countdown_server.cpp")
add_ros_deps("rclcpp", "rclcpp_action", "demo_xmake_interfaces")

target("countdown_client")
set_kind("binary")
set_installdir("lib/demo_xmake_action")
add_files("src/countdown_client.cpp")
add_ros_deps("rclcpp", "rclcpp_action", "demo_xmake_interfaces")
17 changes: 17 additions & 0 deletions demo_xmake_benchmark/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>demo_xmake_benchmark</name>
<version>0.1.0</version>
<description>Communication performance benchmark demo with xmake.</description>
<maintainer email="devnull@example.com">ros2_xmake maintainers</maintainer>
<license>Apache License 2.0</license>

<buildtool_depend>ament_xmake</buildtool_depend>
<depend>rclcpp</depend>
<depend>std_msgs</depend>

<export>
<build_type>ament_xmake</build_type>
</export>
</package>
55 changes: 55 additions & 0 deletions demo_xmake_benchmark/src/latency_ping.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#include <chrono>
#include <string>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

class LatencyPing : public rclcpp::Node
{
public:
LatencyPing()
: Node("latency_ping")
{
publisher_ = this->create_publisher<std_msgs::msg::String>("/benchmark/ping", 10);
subscription_ = this->create_subscription<std_msgs::msg::String>(
"/benchmark/pong", 10,
std::bind(&LatencyPing::pong_callback, this, std::placeholders::_1));
timer_ = this->create_wall_timer(
std::chrono::milliseconds(100),
std::bind(&LatencyPing::timer_callback, this));
RCLCPP_INFO(this->get_logger(), "LatencyPing started");
}

private:
void timer_callback()
{
auto now = std::chrono::steady_clock::now();
auto ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
now.time_since_epoch()).count();
auto msg = std_msgs::msg::String();
msg.data = std::to_string(ns);
publisher_->publish(msg);
}

void pong_callback(const std_msgs::msg::String::SharedPtr msg)
{
auto now = std::chrono::steady_clock::now();
auto send_ns = std::stoll(msg->data);
auto now_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
now.time_since_epoch()).count();
double latency_ms = static_cast<double>(now_ns - send_ns) / 1e6;
RCLCPP_INFO(this->get_logger(), "Round-trip latency: %.2f ms", latency_ms);
}

rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<LatencyPing>());
rclcpp::shutdown();
return 0;
}
Loading
Loading