6.2 C++接口使用示例

本章节将带您实现索引所示若干功能

构建 & 运行说明

  • 进入到解压后的 sdk 顶层目录,执行以下命令

    source /opt/ros/humble/setup.bash
    colcon build
    source install/setup.bash
    ros2 run examples '对应功能名称如: get_mc_action'
    

📝 代码说明

完整的代码实现包含完整的错误处理、信号处理、超时处理等机制,确保程序的健壮性。请您在 examples 目录下查看/修改

小心

ROS的服务(service)机制在跨板通信时存在一些待优化问题, 二次开发时请参考例程添加异常处理、快速重试等保护机制

6.2.1 获取机器人模式

通过调用GetMcAction服务获取机器人当前的运行模式,包括名称和状态信息。

运动模式定义

  1#include "aimdk_msgs/srv/get_mc_action.hpp"
  2#include "aimdk_msgs/msg/common_request.hpp"
  3#include "aimdk_msgs/msg/response_header.hpp"
  4#include "rclcpp/rclcpp.hpp"
  5#include <chrono>
  6#include <memory>
  7#include <signal.h>
  8
  9// Global variable used for signal handling
 10std::shared_ptr<rclcpp::Node> g_node = nullptr;
 11
 12// Signal handler function
 13void signal_handler(int signal) {
 14  if (g_node) {
 15    RCLCPP_INFO(g_node->get_logger(), "Received signal %d, shutting down...",
 16                signal);
 17    g_node.reset();
 18  }
 19  rclcpp::shutdown();
 20  exit(signal);
 21}
 22
 23class GetMcActionClient : public rclcpp::Node {
 24public:
 25  GetMcActionClient() : Node("get_mc_action_client") {
 26
 27    client_ = this->create_client<aimdk_msgs::srv::GetMcAction>(
 28        "/aimdk_5Fmsgs/srv/GetMcAction"); // correct the service path
 29    RCLCPP_INFO(this->get_logger(), "✅ GetMcAction client node created.");
 30
 31    while (!client_->wait_for_service(std::chrono::seconds(2))) {
 32      RCLCPP_INFO(this->get_logger(), "⏳ Service unavailable, waiting...");
 33    }
 34    RCLCPP_INFO(this->get_logger(),
 35                "🟢 Service available, ready to send request.");
 36  }
 37
 38  void send_request() {
 39    try {
 40      auto request = std::make_shared<aimdk_msgs::srv::GetMcAction::Request>();
 41      request->request = aimdk_msgs::msg::CommonRequest();
 42
 43      RCLCPP_INFO(this->get_logger(), "📨 Sending request to get robot mode");
 44
 45      // Set a service call timeout
 46      const std::chrono::milliseconds timeout(250);
 47      for (int i = 0; i < 8; i++) {
 48        request->request.header.stamp = this->now();
 49        auto future = client_->async_send_request(request);
 50        auto retcode = rclcpp::spin_until_future_complete(shared_from_this(),
 51                                                          future, timeout);
 52        if (retcode != rclcpp::FutureReturnCode::SUCCESS) {
 53          // retry as remote peer is NOT handled well by ROS
 54          RCLCPP_INFO(this->get_logger(), "trying ... [%d]", i);
 55          continue;
 56        }
 57        // future.done
 58        auto response = future.get();
 59        RCLCPP_INFO(this->get_logger(), "✅ Robot mode get successfully.");
 60        RCLCPP_INFO(this->get_logger(), "Mode name: %s",
 61                    response->info.action_desc.c_str());
 62        RCLCPP_INFO(this->get_logger(), "Mode status: %d",
 63                    response->info.status.value);
 64        return;
 65      }
 66      RCLCPP_ERROR(this->get_logger(), "❌ Service call failed or timed out.");
 67    } catch (const std::exception &e) {
 68      RCLCPP_ERROR(this->get_logger(), "Exception occurred: %s", e.what());
 69    }
 70  }
 71
 72private:
 73  rclcpp::Client<aimdk_msgs::srv::GetMcAction>::SharedPtr client_;
 74};
 75
 76int main(int argc, char *argv[]) {
 77  try {
 78    rclcpp::init(argc, argv);
 79
 80    // Set up signal handlers
 81    signal(SIGINT, signal_handler);
 82    signal(SIGTERM, signal_handler);
 83
 84    // Create node
 85    g_node = std::make_shared<GetMcActionClient>();
 86    auto client = std::dynamic_pointer_cast<GetMcActionClient>(g_node);
 87    if (client) {
 88      client->send_request();
 89    }
 90
 91    // Clean up resources
 92    g_node.reset();
 93    rclcpp::shutdown();
 94
 95    return 0;
 96  } catch (const std::exception &e) {
 97    RCLCPP_ERROR(rclcpp::get_logger("main"),
 98                 "Program exited with exception: %s", e.what());
 99    return 1;
100  }
101}

使用说明

ros2 run examples get_mc_action

输出示例

...
[INFO] [1764066631.021247791] [get_mc_action_client]: Current robot mode:
[INFO] [1764066631.021832667] [get_mc_action_client]: Mode name: PASSIVE_DEFAULT
[INFO] [1764066631.022396136] [get_mc_action_client]: Mode status: 100

接口参考

  • 服务:/aimdk_5Fmsgs/srv/GetMcAction

  • 消息:aimdk_msgs/srv/GetMcAction

6.2.2 设置机器人模式

该示例中用到了SetMcAction服务,运行节点程序后终端输入模式对应的字段值,机器人会尝试进入相应的运动模式
切入稳定站立(STAND_DEFAULT)模式前,确保机器人已立起且双脚着地。
模式切换需遵循模式转换图, 跨模式转换不能成功
走跑模式(LOCOMOTION_DEFAULT)和稳定站立模式一体化自动切换,按模式转换图流转到两者中较近的即可

  1#include "aimdk_msgs/srv/set_mc_action.hpp"
  2#include "aimdk_msgs/msg/common_response.hpp"
  3#include "aimdk_msgs/msg/common_state.hpp"
  4#include "aimdk_msgs/msg/mc_action.hpp"
  5#include "aimdk_msgs/msg/mc_action_command.hpp"
  6#include "aimdk_msgs/msg/request_header.hpp"
  7#include "rclcpp/rclcpp.hpp"
  8#include <chrono>
  9#include <iomanip>
 10#include <memory>
 11#include <signal.h>
 12#include <unordered_map>
 13#include <vector>
 14
 15// Global variable used for signal handling
 16std::shared_ptr<rclcpp::Node> g_node = nullptr;
 17
 18// Signal handler function
 19void signal_handler(int signal) {
 20  if (g_node) {
 21    RCLCPP_INFO(g_node->get_logger(), "Received signal %d, shutting down...",
 22                signal);
 23    g_node.reset();
 24  }
 25  rclcpp::shutdown();
 26  exit(signal);
 27}
 28
 29class SetMcActionClient : public rclcpp::Node {
 30public:
 31  SetMcActionClient() : Node("set_mc_action_client") {
 32
 33    client_ = this->create_client<aimdk_msgs::srv::SetMcAction>(
 34        "/aimdk_5Fmsgs/srv/SetMcAction");
 35    RCLCPP_INFO(this->get_logger(), "✅ SetMcAction client node created.");
 36
 37    // Wait for the service to become available
 38    while (!client_->wait_for_service(std::chrono::seconds(2))) {
 39      RCLCPP_INFO(this->get_logger(), "⏳ Service unavailable, waiting...");
 40    }
 41    RCLCPP_INFO(this->get_logger(),
 42                "🟢 Service available, ready to send request.");
 43  }
 44
 45  bool send_request(std::string &action_name) {
 46    try {
 47      auto request = std::make_shared<aimdk_msgs::srv::SetMcAction::Request>();
 48      request->header = aimdk_msgs::msg::RequestHeader();
 49
 50      // Set robot mode
 51      aimdk_msgs::msg::McActionCommand command;
 52      command.action_desc = action_name;
 53      request->command = command;
 54
 55      RCLCPP_INFO(this->get_logger(), "📨 Sending request to set robot mode: %s",
 56                  action_name.c_str());
 57
 58      // Set Service Call Timeout
 59      const std::chrono::milliseconds timeout(250);
 60      for (int i = 0; i < 8; i++) {
 61        request->header.stamp = this->now();
 62        auto future = client_->async_send_request(request);
 63        auto retcode = rclcpp::spin_until_future_complete(shared_from_this(),
 64                                                          future, timeout);
 65        if (retcode != rclcpp::FutureReturnCode::SUCCESS) {
 66          // retry as remote peer is NOT handled well by ROS
 67          RCLCPP_INFO(this->get_logger(), "trying ... [%d]", i);
 68          continue;
 69        }
 70        // future.done
 71        auto response = future.get();
 72        if (response->response.status.value ==
 73            aimdk_msgs::msg::CommonState::SUCCESS) {
 74          RCLCPP_INFO(this->get_logger(), "✅ Robot mode set successfully.");
 75          return true;
 76        } else {
 77          RCLCPP_ERROR(this->get_logger(), "❌ Failed to set robot mode: %s",
 78                       response->response.message.c_str());
 79          return false;
 80        }
 81      }
 82      RCLCPP_ERROR(this->get_logger(), "❌ Service call failed or timed out.");
 83      return false;
 84    } catch (const std::exception &e) {
 85      RCLCPP_ERROR(this->get_logger(), "Exception occurred: %s", e.what());
 86      return false;
 87    }
 88  }
 89
 90private:
 91  rclcpp::Client<aimdk_msgs::srv::SetMcAction>::SharedPtr client_;
 92};
 93
 94static std::unordered_map<std::string, std::vector<std::string>> g_action_info =
 95    {
 96        {"PASSIVE_DEFAULT", {"PD", "joints with zero torque"}},
 97        {"DAMPING_DEFAULT", {"DD", "joints in damping mode"}},
 98        {"JOINT_DEFAULT", {"JD", "Position Control Stand (joints locked)"}},
 99        {"STAND_DEFAULT", {"SD", "Stable Stand (auto-balance)"}},
100        {"LOCOMOTION_DEFAULT", {"LD", "locomotion mode (walk or run)"}},
101};
102
103int main(int argc, char *argv[]) {
104  try {
105    rclcpp::init(argc, argv);
106
107    // Set up signal handlers
108    signal(SIGINT, signal_handler);
109    signal(SIGTERM, signal_handler);
110
111    // Create node
112    g_node = std::make_shared<SetMcActionClient>();
113    auto client = std::dynamic_pointer_cast<SetMcActionClient>(g_node);
114
115    if (client) {
116      std::unordered_map<std::string, std::string> choices;
117      std::string motion;
118
119      // Prefer command-line argument; otherwise prompt user
120      if (argc > 1) {
121        motion = argv[1];
122        RCLCPP_INFO(g_node->get_logger(),
123                    "Using abbr of motion mode from cmdline: %s", argv[1]);
124      } else {
125        std::cout << std::left << std::setw(4) << "abbr"
126                  << " - " << std::setw(20) << "robot mode"
127                  << " : "
128                  << "description" << std::endl;
129        for (auto &it : g_action_info) {
130          std::cout << std::left << std::setw(4) << it.second[0] << " - "
131                    << std::setw(20) << it.first << " : " << it.second[1]
132                    << std::endl;
133        }
134        std::cout << "Enter abbr of motion mode:";
135        std::cin >> motion;
136      }
137      for (auto &it : g_action_info) {
138        choices[it.second[0]] = it.first;
139      }
140
141      auto m = choices.find(motion);
142      if (m != choices.end()) {
143        auto &action_name = m->second;
144        client->send_request(action_name);
145      } else {
146        RCLCPP_ERROR(g_node->get_logger(), "Invalid abbr of robot mode: %s",
147                     motion.c_str());
148      }
149    }
150
151    // Clean up resources
152    g_node.reset();
153    rclcpp::shutdown();
154
155    return 0;
156  } catch (const std::exception &e) {
157    RCLCPP_ERROR(rclcpp::get_logger("main"),
158                 "Program exited with exception: %s", e.what());
159    return 1;
160  }
161}

使用说明

# 使用命令行参数设置模式(推荐)
ros2 run examples set_mc_action JD  # 零力矩模式>>站姿预备(位控站立)
ros2 run examples set_mc_action SD  # 机器人已立起且脚着地后方可执行,站姿预备>>稳定站立
# 稳定站立>>走跑模式 自动切换,无需手动切换

# 或者不带参数运行,程序会提示用户输入双字母缩写代码
ros2 run examples set_mc_action

输出示例

...
[INFO] [1764066567.502968540] [set_mc_action_client]: ✅ Robot mode set successfully.

注意事项

  • 切入STAND_DEFAULT模式前,确保机器人脚已经着地

  • 模式切换可能需要几秒钟时间完成

接口参考

  • 服务:/aimdk_5Fmsgs/srv/SetMcAction

  • 消息:aimdk_msgs/srv/SetMcAction

6.2.3 设置机器人动作

该示例中用到了preset_motion_client,进入稳定站立模式后,启动节点程序,输入相应的字段值可实现左手(右手)的握手(举手、挥手、飞吻)动作。

可供调用的参数见预设动作表

  1#include "aimdk_msgs/msg/common_response.hpp"
  2#include "aimdk_msgs/msg/common_state.hpp"
  3#include "aimdk_msgs/msg/common_task_response.hpp"
  4#include "aimdk_msgs/msg/mc_control_area.hpp"
  5#include "aimdk_msgs/msg/mc_preset_motion.hpp"
  6#include "aimdk_msgs/msg/request_header.hpp"
  7#include "aimdk_msgs/srv/set_mc_preset_motion.hpp"
  8#include "rclcpp/rclcpp.hpp"
  9#include <chrono>
 10#include <memory>
 11#include <signal.h>
 12
 13std::shared_ptr<rclcpp::Node> g_node = nullptr;
 14
 15void signal_handler(int signal) {
 16  if (g_node) {
 17    RCLCPP_INFO(g_node->get_logger(), "Received signal %d, shutting down...",
 18                signal);
 19    g_node.reset();
 20  }
 21  rclcpp::shutdown();
 22  exit(signal);
 23}
 24
 25class PresetMotionClient : public rclcpp::Node {
 26public:
 27  PresetMotionClient() : Node("preset_motion_client") {
 28    const std::chrono::seconds timeout(8);
 29
 30    client_ = this->create_client<aimdk_msgs::srv::SetMcPresetMotion>(
 31        "/aimdk_5Fmsgs/srv/SetMcPresetMotion");
 32
 33    RCLCPP_INFO(this->get_logger(), "✅ SetMcPresetMotion client node created.");
 34
 35    // Wait for the service to become available
 36    while (!client_->wait_for_service(std::chrono::seconds(2))) {
 37      RCLCPP_INFO(this->get_logger(), "⏳ Service unavailable, waiting...");
 38    }
 39    RCLCPP_INFO(this->get_logger(),
 40                "🟢 Service available, ready to send request.");
 41  }
 42
 43  bool send_request(int area_id, int motion_id) {
 44    try {
 45      auto request =
 46          std::make_shared<aimdk_msgs::srv::SetMcPresetMotion::Request>();
 47      request->header = aimdk_msgs::msg::RequestHeader();
 48
 49      aimdk_msgs::msg::McPresetMotion motion;
 50      aimdk_msgs::msg::McControlArea area;
 51
 52      motion.value = motion_id; // Preset motion ID
 53      area.value = area_id;     // Control area ID
 54      request->motion = motion;
 55      request->area = area;
 56      request->interrupt = false; // Not interrupt current motion
 57
 58      RCLCPP_INFO(this->get_logger(),
 59                  "📨 Sending request to set preset motion: motion=%d, area=%d",
 60                  motion_id, area_id);
 61
 62      const std::chrono::milliseconds timeout(250);
 63      for (int i = 0; i < 8; i++) {
 64        request->header.stamp = this->now();
 65        auto future = client_->async_send_request(request);
 66        auto retcode = rclcpp::spin_until_future_complete(shared_from_this(),
 67                                                          future, timeout);
 68        if (retcode != rclcpp::FutureReturnCode::SUCCESS) {
 69          // retry as remote peer is NOT handled well by ROS
 70          RCLCPP_INFO(this->get_logger(), "trying ... [%d]", i);
 71          continue;
 72        }
 73        // future.done
 74        auto response = future.get();
 75        if (response->response.header.code == 0) {
 76          RCLCPP_INFO(this->get_logger(),
 77                      "✅ Preset motion set successfully: %lu",
 78                      response->response.task_id);
 79          return true;
 80        } else if (response->response.state.value ==
 81                   aimdk_msgs::msg::CommonState::RUNNING) {
 82          RCLCPP_INFO(this->get_logger(), "⏳ Preset motion executing: %lu",
 83                      response->response.task_id);
 84          return true;
 85        } else {
 86          RCLCPP_WARN(this->get_logger(), "❌ Failed to set preset motion: %lu",
 87                      response->response.task_id);
 88          return false;
 89        }
 90      }
 91      RCLCPP_ERROR(this->get_logger(), "❌ Service call failed or timed out.");
 92      return false;
 93    } catch (const std::exception &e) {
 94      RCLCPP_ERROR(this->get_logger(), "Exception occurred: %s", e.what());
 95      return false;
 96    }
 97  }
 98
 99private:
100  rclcpp::Client<aimdk_msgs::srv::SetMcPresetMotion>::SharedPtr client_;
101};
102
103int main(int argc, char *argv[]) {
104  try {
105    rclcpp::init(argc, argv);
106    signal(SIGINT, signal_handler);
107    signal(SIGTERM, signal_handler);
108
109    g_node = std::make_shared<PresetMotionClient>();
110    // Cast g_node (std::shared_ptr<rclcpp::Node>) to a derived
111    // PresetMotionClient pointer (std::shared_ptr<PresetMotionClient>)
112    auto client = std::dynamic_pointer_cast<PresetMotionClient>(g_node);
113
114    int area = 1;
115    int motion = 1003;
116    std::cout << "Enter arm area ID (1-left, 2-right): ";
117    std::cin >> area;
118    std::cout
119        << "Enter preset motion ID (1001-raise, 1002-wave, 1003-handshake, "
120           "1004-airkiss): ";
121    std::cin >> motion;
122    if (client) {
123      client->send_request(area, motion);
124    }
125
126    // Clean up resources
127    g_node.reset();
128    rclcpp::shutdown();
129
130    return 0;
131  } catch (const std::exception &e) {
132    RCLCPP_ERROR(rclcpp::get_logger("main"),
133                 "Program exited with exception: %s", e.what());
134    return 1;
135  }
136}

6.2.4 夹爪控制

该示例中用到了hand_control,通过往话题/aima/hal/joint/hand/command发布消息来控制夹爪的运动

注意

注意⚠️ :运行本示例前需要在机器人运控计算单元(PC1)使用aima em stop-app mc关闭原生运控模块,获取机器人控制权。注意确保机器人安全

 1#include "aimdk_msgs/msg/hand_command.hpp"
 2#include "aimdk_msgs/msg/hand_command_array.hpp"
 3#include "aimdk_msgs/msg/hand_type.hpp"
 4#include "aimdk_msgs/msg/message_header.hpp"
 5#include "rclcpp/rclcpp.hpp"
 6#include <chrono>
 7#include <vector>
 8
 9class HandControl : public rclcpp::Node {
10public:
11  HandControl()
12      : Node("hand_control"), position_pairs_({
13                                  {1.0, 1.0},
14                                  {0.0, 0.0},
15                                  {0.5, 0.5},
16                                  {0.2, 0.8},
17                                  {0.7, 0.3},
18                              }),
19        current_index_(0) {
20    publisher_ = this->create_publisher<aimdk_msgs::msg::HandCommandArray>(
21        "/aima/hal/joint/hand/command", 10);
22
23    timer_ = this->create_wall_timer(
24        std::chrono::milliseconds(20), // 50Hz
25        std::bind(&HandControl::publish_hand_commands, this));
26
27    last_switch_time_ = now();
28    RCLCPP_INFO(this->get_logger(), "The hand control node has been started!");
29  }
30
31  void publish_hand_commands() {
32    // 1. Determine if it's time to switch parameters.
33    auto now_time = this->now();
34    if ((now_time - last_switch_time_).seconds() >= 2.0) {
35      current_index_ = (current_index_ + 1) % position_pairs_.size();
36      last_switch_time_ = now_time;
37      RCLCPP_INFO(this->get_logger(),
38                  "Switched to the next parameter group, index=%zu (left=%.2f, "
39                  "right=%.2f)",
40                  current_index_, position_pairs_[current_index_].first,
41                  position_pairs_[current_index_].second);
42    }
43
44    auto msg = std::make_unique<aimdk_msgs::msg::HandCommandArray>();
45    msg->header = aimdk_msgs::msg::MessageHeader();
46
47    float left_position = position_pairs_[current_index_].first;
48    float right_position = position_pairs_[current_index_].second;
49
50    aimdk_msgs::msg::HandCommand left_hands;
51    left_hands.name = "left_hand";
52    left_hands.position = left_position;
53    left_hands.velocity = 1.0;
54    left_hands.acceleration = 1.0;
55    left_hands.deceleration = 1.0;
56    left_hands.effort = 1.0;
57
58    aimdk_msgs::msg::HandCommand right_hands;
59    right_hands.name = "right_hand";
60    right_hands.position = right_position;
61    right_hands.velocity = 1.0;
62    right_hands.acceleration = 1.0;
63    right_hands.deceleration = 1.0;
64    right_hands.effort = 1.0;
65
66    msg->left_hands.push_back(left_hands);
67    msg->right_hands.push_back(right_hands);
68    msg->left_hand_type.value = 2;
69    msg->right_hand_type.value = 2;
70
71    publisher_->publish(std::move(msg));
72  }
73
74private:
75  rclcpp::Publisher<aimdk_msgs::msg::HandCommandArray>::SharedPtr publisher_;
76  rclcpp::TimerBase::SharedPtr timer_;
77
78  std::vector<std::pair<float, float>> position_pairs_;
79  size_t current_index_;
80
81  rclcpp::Time last_switch_time_;
82};
83
84int main(int argc, char *argv[]) {
85  rclcpp::init(argc, argv);
86  auto hand_control_node = std::make_shared<HandControl>();
87  rclcpp::spin(hand_control_node);
88  rclcpp::shutdown();
89  return 0;
90}

6.2.5 灵巧手控制

该示例中用到了omnihand_control,通过往话题/aima/hal/joint/hand/command发布消息来控制灵巧手的运动

注意

注意⚠️ :运行本示例前需要在机器人运控计算单元(PC1)使用aima em stop-app mc关闭原生运控模块,获取机器人控制权。注意确保机器人安全

  1#include <aimdk_msgs/msg/hand_command_array.hpp>
  2#include <chrono>
  3#include <cmath>
  4#include <memory>
  5#include <rclcpp/rclcpp.hpp>
  6
  7using namespace std::chrono_literals;
  8
  9class HandCommandPublisher : public rclcpp::Node {
 10public:
 11  HandCommandPublisher() : Node("hand_command_publisher") {
 12    publisher_ = this->create_publisher<aimdk_msgs::msg::HandCommandArray>(
 13        "/aima/hal/joint/hand/command", 10);
 14
 15    // Create a timer to publish once per second
 16    timer_ = this->create_wall_timer(
 17        1s, std::bind(&HandCommandPublisher::publish_command, this));
 18
 19    // Create a timer to publish once per second
 20    int target_finger = 0;
 21    int step_ = 1;
 22    bool increasing_ = true;
 23  }
 24
 25private:
 26  void publish_command() {
 27    auto message = aimdk_msgs::msg::HandCommandArray();
 28
 29    // Set hander
 30    message.header.stamp = this->now();
 31    message.header.frame_id = "hand_command";
 32
 33    // Set the hand type
 34    message.left_hand_type.value = 1;  // NIMBLE_HANDS
 35    message.right_hand_type.value = 1; // NIMBLE_HANDS
 36
 37    // Create left hand command array
 38    message.left_hands.resize(10);
 39
 40    // Set left thumb
 41    message.left_hands[0].name = "left_thumb";
 42    message.left_hands[0].position = 0.0;
 43    message.left_hands[0].velocity = 0.1;
 44    message.left_hands[0].acceleration = 0.0;
 45    message.left_hands[0].deceleration = 0.0;
 46    message.left_hands[0].effort = 0.0;
 47    // Set other left fingers
 48    for (int i = 1; i < 10; i++) {
 49      message.left_hands[i].name = "left_index";
 50      message.left_hands[i].position = 0.0;
 51      message.left_hands[i].velocity = 0.1;
 52      message.left_hands[i].acceleration = 0.0;
 53      message.left_hands[i].deceleration = 0.0;
 54      message.left_hands[i].effort = 0.0;
 55    }
 56
 57    // Create right hand command array
 58    message.right_hands.resize(10);
 59
 60    // Set right thumb
 61    message.right_hands[0].name = "right_thumb";
 62    message.right_hands[0].position = 0.0;
 63    message.right_hands[0].velocity = 0.1;
 64    message.right_hands[0].acceleration = 0.0;
 65    message.right_hands[0].deceleration = 0.0;
 66    message.right_hands[0].effort = 0.0;
 67
 68    // Set other right fingers (pinky)
 69    for (int i = 1; i < 10; i++) {
 70      message.right_hands[i].name = "right_pinky";
 71      message.right_hands[i].position = 0.0;
 72      message.right_hands[i].velocity = 0.1;
 73      message.right_hands[i].acceleration = 0.0;
 74      message.right_hands[i].deceleration = 0.0;
 75      message.right_hands[i].effort = 0.0;
 76    }
 77
 78    if (target_finger <= 10) {
 79      message.right_hands[target_finger].position = 0.8;
 80    } else {
 81      int target_finger_ = target_finger - 10;
 82      double target_position = 0.8;
 83      if (target_finger_ < 3) {
 84        // The three thumb motors on the left hand need their signs inverted to
 85        // mirror the right hand's motion
 86        target_position = -target_position;
 87      }
 88      message.left_hands[target_finger_].position = target_position;
 89    }
 90
 91    // Publish the message
 92    publisher_->publish(message);
 93
 94    RCLCPP_INFO(this->get_logger(),
 95                "Published hand command with target_finger: %d", target_finger);
 96
 97    update_target_finger();
 98  }
 99
100  void update_target_finger() {
101    if (increasing_) {
102      target_finger += step_;
103      if (target_finger >= 19) {
104        target_finger = 19;
105        increasing_ = false;
106      }
107    } else {
108      target_finger -= step_;
109      if (target_finger <= 0) {
110        target_finger = 0;
111        increasing_ = true;
112      }
113    }
114  }
115
116  rclcpp::Publisher<aimdk_msgs::msg::HandCommandArray>::SharedPtr publisher_;
117  rclcpp::TimerBase::SharedPtr timer_;
118
119  int target_finger = 0;
120  int step_ = 1;
121  bool increasing_ = true;
122};
123
124int main(int argc, char **argv) {
125  rclcpp::init(argc, argv);
126  auto node = std::make_shared<HandCommandPublisher>();
127  rclcpp::spin(node);
128  rclcpp::shutdown();
129  return 0;
130}

6.2.6 注册二开输入源

对于v0.7之后的版本,在实现对MC的控制前,需要先注册输入源。该示例中通过/aimdk_5Fmsgs/srv/SetMcInputSource服务来注册二开的输入源,让MC感知到,只有注册过输入源后才能实现机器人的速度控制

  1#include "aimdk_msgs/srv/set_mc_input_source.hpp"
  2#include "aimdk_msgs/msg/common_request.hpp"
  3#include "aimdk_msgs/msg/common_response.hpp"
  4#include "aimdk_msgs/msg/common_state.hpp"
  5#include "aimdk_msgs/msg/common_task_response.hpp"
  6#include "aimdk_msgs/msg/mc_input_action.hpp"
  7#include "rclcpp/rclcpp.hpp"
  8#include <chrono>
  9#include <memory>
 10#include <signal.h>
 11
 12std::shared_ptr<rclcpp::Node> g_node = nullptr;
 13
 14void signal_handler(int signal) {
 15  if (g_node) {
 16    RCLCPP_INFO(g_node->get_logger(), "Received signal %d, shutting down...",
 17                signal);
 18    g_node.reset();
 19  }
 20  rclcpp::shutdown();
 21  exit(signal);
 22}
 23
 24class McInputClient : public rclcpp::Node {
 25public:
 26  McInputClient() : Node("set_mc_input_source_client") {
 27    client_ = this->create_client<aimdk_msgs::srv::SetMcInputSource>(
 28        "/aimdk_5Fmsgs/srv/SetMcInputSource");
 29
 30    RCLCPP_INFO(this->get_logger(), "✅ SetMcInputSource client node created.");
 31
 32    // Wait for the service to become available
 33    while (!client_->wait_for_service(std::chrono::seconds(2))) {
 34      RCLCPP_INFO(this->get_logger(), "⏳ Service unavailable, waiting...");
 35    }
 36    RCLCPP_INFO(this->get_logger(),
 37                "🟢 Service available, ready to send request.");
 38  }
 39
 40  bool send_request() {
 41    try {
 42      auto request =
 43          std::make_shared<aimdk_msgs::srv::SetMcInputSource::Request>();
 44
 45      // Set request data
 46      request->action.value = 1001;         // Add new input source
 47      request->input_source.name = "node";  // Set message source
 48      request->input_source.priority = 40;  // Set priority
 49      request->input_source.timeout = 1000; // Set timeout (ms)
 50
 51      RCLCPP_INFO(this->get_logger(), "📨 Sending input source request: (ID=%d)",
 52                  request->action.value);
 53
 54      auto timeout = std::chrono::milliseconds(250);
 55      for (int i = 0; i < 8; i++) {
 56        // Set header timestamp
 57        request->request.header.stamp = this->now(); // use Node::now()
 58        auto future = client_->async_send_request(request);
 59        auto retcode = rclcpp::spin_until_future_complete(
 60            this->shared_from_this(), future, timeout);
 61        if (retcode != rclcpp::FutureReturnCode::SUCCESS) {
 62          // retry as remote peer is NOT handled well by ROS
 63          RCLCPP_INFO(this->get_logger(), "trying ... [%d]", i);
 64          continue;
 65        }
 66        // future.done
 67        auto response = future.get();
 68        auto code = response->response.header.code;
 69        if (code == 0) {
 70          RCLCPP_INFO(this->get_logger(),
 71                      "✅ Input source set successfully. task_id=%lu",
 72                      response->response.task_id);
 73          return true;
 74        } else {
 75          RCLCPP_ERROR(
 76              this->get_logger(),
 77              "❌ Input source set failed. ret_code=%ld, task_id=%lu "
 78              "(duplicated ADD? or MODIFY/ENABLE/DISABLE for unknown source?)",
 79              code, response->response.task_id);
 80          return false;
 81        }
 82      }
 83      RCLCPP_ERROR(this->get_logger(), "❌ Service call failed or timed out.");
 84      return false;
 85    } catch (const std::exception &e) {
 86      RCLCPP_ERROR(this->get_logger(), "Exception occurred: %s", e.what());
 87      return false;
 88    }
 89  }
 90
 91private:
 92  rclcpp::Client<aimdk_msgs::srv::SetMcInputSource>::SharedPtr client_;
 93};
 94
 95int main(int argc, char *argv[]) {
 96  try {
 97    rclcpp::init(argc, argv);
 98    signal(SIGINT, signal_handler);
 99    signal(SIGTERM, signal_handler);
100
101    g_node = std::make_shared<McInputClient>();
102    auto client = std::dynamic_pointer_cast<McInputClient>(g_node);
103
104    if (client) {
105      client->send_request();
106    }
107
108    g_node.reset();
109    rclcpp::shutdown();
110
111    return 0;
112  } catch (const std::exception &e) {
113    RCLCPP_ERROR(rclcpp::get_logger("main"),
114                 "Program terminated with exception: %s", e.what());
115    return 1;
116  }
117}

6.2.7 获取当前输入源

该示例中用到了GetCurrentInputSource服务,用于获取当前已注册的输入源信息,包括输入源名称、优先级和超时时间等信息。

  1#include "aimdk_msgs/srv/get_current_input_source.hpp"
  2#include "aimdk_msgs/msg/common_request.hpp"
  3#include "aimdk_msgs/msg/response_header.hpp"
  4#include "rclcpp/rclcpp.hpp"
  5#include <chrono>
  6#include <memory>
  7#include <signal.h>
  8
  9// Global node object
 10std::shared_ptr<rclcpp::Node> g_node = nullptr;
 11
 12// Signal handler
 13void signal_handler(int signal) {
 14  if (g_node) {
 15    RCLCPP_INFO(g_node->get_logger(), "Received signal %d, shutting down...",
 16                signal);
 17    g_node.reset();
 18  }
 19  rclcpp::shutdown();
 20  exit(signal);
 21}
 22
 23// Client Class
 24class GetCurrentInputSourceClient : public rclcpp::Node {
 25public:
 26  GetCurrentInputSourceClient() : Node("get_current_input_source_client") {
 27
 28    client_ = this->create_client<aimdk_msgs::srv::GetCurrentInputSource>(
 29        "/aimdk_5Fmsgs/srv/GetCurrentInputSource");
 30
 31    RCLCPP_INFO(this->get_logger(),
 32                "✅ GetCurrentInputSource client node created.");
 33
 34    // Wait for the service to become available
 35    while (!client_->wait_for_service(std::chrono::seconds(2))) {
 36      RCLCPP_INFO(this->get_logger(), "⏳ Service unavailable, waiting...");
 37    }
 38    RCLCPP_INFO(this->get_logger(),
 39                "🟢 Service available, ready to send request.");
 40  }
 41
 42  void send_request() {
 43    try {
 44      auto request =
 45          std::make_shared<aimdk_msgs::srv::GetCurrentInputSource::Request>();
 46      request->request = aimdk_msgs::msg::CommonRequest();
 47
 48      RCLCPP_INFO(this->get_logger(),
 49                  "📨 Sending request to get current input source");
 50
 51      auto timeout = std::chrono::milliseconds(250);
 52
 53      for (int i = 0; i < 8; i++) {
 54        request->request.header.stamp = this->now();
 55        auto future = client_->async_send_request(request);
 56        auto retcode = rclcpp::spin_until_future_complete(
 57            this->shared_from_this(), future, timeout);
 58        if (retcode != rclcpp::FutureReturnCode::SUCCESS) {
 59          // retry as remote peer is NOT handled well by ROS
 60          RCLCPP_INFO(this->get_logger(), "trying ... [%d]", i);
 61          continue;
 62        }
 63        // future.done
 64        auto response = future.get();
 65        if (response->response.header.code == 0) {
 66          RCLCPP_INFO(this->get_logger(),
 67                      "✅ Current input source get successfully:");
 68          RCLCPP_INFO(this->get_logger(), "Name: %s",
 69                      response->input_source.name.c_str());
 70          RCLCPP_INFO(this->get_logger(), "Priority: %d",
 71                      response->input_source.priority);
 72          RCLCPP_INFO(this->get_logger(), "Timeout: %d",
 73                      response->input_source.timeout);
 74        } else {
 75          RCLCPP_WARN(this->get_logger(),
 76                      "❌ Current input source get failed, return code: %ld",
 77                      response->response.header.code);
 78        }
 79        return;
 80      }
 81      RCLCPP_ERROR(this->get_logger(), "❌ Service call failed or timed out.");
 82    } catch (const std::exception &e) {
 83      RCLCPP_ERROR(this->get_logger(), "Exception occurred: %s", e.what());
 84    }
 85  }
 86
 87private:
 88  rclcpp::Client<aimdk_msgs::srv::GetCurrentInputSource>::SharedPtr client_;
 89};
 90
 91int main(int argc, char *argv[]) {
 92  try {
 93    rclcpp::init(argc, argv);
 94
 95    signal(SIGINT, signal_handler);
 96    signal(SIGTERM, signal_handler);
 97
 98    g_node = std::make_shared<GetCurrentInputSourceClient>();
 99    auto client =
100        std::dynamic_pointer_cast<GetCurrentInputSourceClient>(g_node);
101
102    if (client) {
103      client->send_request();
104    }
105
106    g_node.reset();
107    rclcpp::shutdown();
108    return 0;
109  } catch (const std::exception &e) {
110    RCLCPP_ERROR(rclcpp::get_logger("main"),
111                 "Program exited with exception: %s", e.what());
112    return 1;
113  }
114}

使用说明

# 获取当前输入源信息
ros2 run examples get_current_input_source

输出示例

[INFO] [get_current_input_source_client]: 当前输入源: node
[INFO] [get_current_input_source_client]: 优先级: 40
[INFO] [get_current_input_source_client]: 超时时间: 1000

注意事项

  • 确保GetCurrentInputSource服务正常运行

  • 需要在注册输入源之后才能获取到有效信息

  • 状态码为0表示查询成功

6.2.8 机器人走跑控制

该示例中用到了mc_locomotion_velocity,以下示例通过发布/aima/mc/locomotion/velocity话题控制机器人的行走,对于v0.7之后的版本,实现速度控制前需要注册输入源(该示例已实现注册输入源),具体注册步骤可参考代码。

进入稳定站立模式后执行本程序

  1#include "aimdk_msgs/msg/mc_locomotion_velocity.hpp"
  2#include "aimdk_msgs/msg/common_request.hpp"
  3#include "aimdk_msgs/msg/common_response.hpp"
  4#include "aimdk_msgs/msg/common_state.hpp"
  5#include "aimdk_msgs/msg/common_task_response.hpp"
  6#include "aimdk_msgs/msg/mc_input_action.hpp"
  7#include "aimdk_msgs/msg/message_header.hpp"
  8#include "aimdk_msgs/srv/set_mc_input_source.hpp"
  9
 10#include "rclcpp/rclcpp.hpp"
 11#include <chrono>
 12#include <memory>
 13#include <signal.h>
 14#include <thread>
 15
 16class DirectVelocityControl : public rclcpp::Node {
 17public:
 18  DirectVelocityControl() : Node("direct_velocity_control") {
 19    // Create publisher
 20    publisher_ = this->create_publisher<aimdk_msgs::msg::McLocomotionVelocity>(
 21        "/aima/mc/locomotion/velocity", 10);
 22    // Create service client
 23    client_ = this->create_client<aimdk_msgs::srv::SetMcInputSource>(
 24        "/aimdk_5Fmsgs/srv/SetMcInputSource");
 25
 26    // Maximum speed limits
 27    max_forward_speed_ = 1.0; // m/s
 28    max_lateral_speed_ = 1.0; // m/s
 29    max_angular_speed_ = 1.0; // rad/s
 30    // Minimum speed limits (0 is also OK)
 31    min_forward_speed_ = 0.2; // m/s
 32    min_lateral_speed_ = 0.2; // m/s
 33    min_angular_speed_ = 0.1; // rad/s
 34
 35    RCLCPP_INFO(this->get_logger(), "Direct velocity control node started.");
 36  }
 37
 38  void start_publish() {
 39    if (timer_ != nullptr) {
 40      return;
 41    }
 42    // Set timer to periodically publish velocity messages (50Hz)
 43    timer_ = this->create_wall_timer(
 44        std::chrono::milliseconds(20),
 45        std::bind(&DirectVelocityControl::publish_velocity, this));
 46  }
 47
 48  bool register_input_source() {
 49    const std::chrono::seconds timeout(8);
 50    auto start_time = std::chrono::steady_clock::now();
 51    while (!client_->wait_for_service(std::chrono::seconds(2))) {
 52      if (std::chrono::steady_clock::now() - start_time > timeout) {
 53        RCLCPP_ERROR(this->get_logger(), "Waiting for service timed out");
 54        return false;
 55      }
 56      RCLCPP_INFO(this->get_logger(), "Waiting for input source service...");
 57    }
 58
 59    auto request =
 60        std::make_shared<aimdk_msgs::srv::SetMcInputSource::Request>();
 61    request->action.value = 1001;
 62    request->input_source.name = "node";
 63    request->input_source.priority = 40;
 64    request->input_source.timeout = 1000;
 65
 66    auto timeout2 = std::chrono::milliseconds(250);
 67
 68    for (int i = 0; i < 8; i++) {
 69      request->request.header.stamp = this->now();
 70      auto future = client_->async_send_request(request);
 71      auto retcode = rclcpp::spin_until_future_complete(
 72          this->shared_from_this(), future, timeout2);
 73      if (retcode != rclcpp::FutureReturnCode::SUCCESS) {
 74        // retry as remote peer is NOT handled well by ROS
 75        RCLCPP_INFO(this->get_logger(),
 76                    "trying to register input source... [%d]", i);
 77        continue;
 78      }
 79      // future.done
 80      auto response = future.get();
 81      int state = response->response.state.value;
 82      RCLCPP_INFO(this->get_logger(),
 83                  "Set input source succeeded: state=%d, task_id=%lu", state,
 84                  response->response.task_id);
 85      return true;
 86    }
 87    RCLCPP_ERROR(this->get_logger(), "Service call failed or timed out");
 88    return false;
 89  }
 90
 91  void publish_velocity() {
 92    auto msg = std::make_unique<aimdk_msgs::msg::McLocomotionVelocity>();
 93    msg->header = aimdk_msgs::msg::MessageHeader();
 94    msg->header.stamp = this->now();
 95    msg->source = "node"; // Set message source
 96    msg->forward_velocity = forward_velocity_;
 97    msg->lateral_velocity = lateral_velocity_;
 98    msg->angular_velocity = angular_velocity_;
 99
100    publisher_->publish(std::move(msg));
101    RCLCPP_INFO(this->get_logger(),
102                "Published velocity: Forward %.2f m/s, Lateral %.2f m/s, "
103                "Angular %.2f rad/s",
104                forward_velocity_, lateral_velocity_, angular_velocity_);
105  }
106
107  void clear_velocity() {
108    forward_velocity_ = 0.0;
109    lateral_velocity_ = 0.0;
110    angular_velocity_ = 0.0;
111  }
112
113  bool set_forward(double forward) {
114    if (abs(forward) < 0.005) {
115      forward_velocity_ = 0.0;
116      return true;
117    } else if ((abs(forward) > max_forward_speed_) ||
118               (abs(forward) < min_forward_speed_)) {
119      RCLCPP_ERROR(this->get_logger(), "input value out of range, exiting");
120      return false;
121    } else {
122      forward_velocity_ = forward;
123      return true;
124    }
125  }
126
127  bool set_lateral(double lateral) {
128    if (abs(lateral) < 0.005) {
129      lateral_velocity_ = 0.0;
130      return true;
131    } else if ((abs(lateral) > max_lateral_speed_) ||
132               (abs(lateral) < min_lateral_speed_)) {
133      RCLCPP_ERROR(this->get_logger(), "input value out of range, exiting");
134      return false;
135    } else {
136      lateral_velocity_ = lateral;
137      return true;
138    }
139  }
140
141  bool set_angular(double angular) {
142    if (abs(angular) < 0.005) {
143      angular_velocity_ = 0.0;
144      return true;
145    } else if ((abs(angular) > max_angular_speed_) ||
146               (abs(angular) < min_angular_speed_)) {
147      RCLCPP_ERROR(this->get_logger(), "input value out of range, exiting");
148      return false;
149    } else {
150      angular_velocity_ = angular;
151      return true;
152    }
153  }
154
155private:
156  rclcpp::Publisher<aimdk_msgs::msg::McLocomotionVelocity>::SharedPtr
157      publisher_;
158  rclcpp::Client<aimdk_msgs::srv::SetMcInputSource>::SharedPtr client_;
159  rclcpp::TimerBase::SharedPtr timer_;
160
161  double forward_velocity_;
162  double lateral_velocity_;
163  double angular_velocity_;
164
165  double max_forward_speed_;
166  double max_lateral_speed_;
167  double max_angular_speed_;
168
169  double min_forward_speed_;
170  double min_lateral_speed_;
171  double min_angular_speed_;
172};
173
174// Signal Processing
175std::shared_ptr<DirectVelocityControl> global_node = nullptr;
176void signal_handler(int sig) {
177  if (global_node) {
178    global_node->clear_velocity();
179    RCLCPP_INFO(global_node->get_logger(),
180                "Received signal %d: clearing velocity and shutting down", sig);
181  }
182  rclcpp::shutdown();
183  exit(sig);
184}
185
186int main(int argc, char *argv[]) {
187  rclcpp::init(argc, argv);
188  signal(SIGINT, signal_handler);
189  signal(SIGTERM, signal_handler);
190
191  global_node = std::make_shared<DirectVelocityControl>();
192  auto node = global_node;
193
194  if (!node->register_input_source()) {
195    RCLCPP_ERROR(node->get_logger(),
196                 "Input source registration failed, exiting");
197    global_node.reset();
198    rclcpp::shutdown();
199    return 1;
200  }
201
202  // get and check control values
203  // notice that mc has thresholds to start movement
204  double forward, lateral, angular;
205  std::cout << "Enter forward speed 0 or ±(0.2 ~ 1.0) m/s: ";
206  std::cin >> forward;
207  if (!node->set_forward(forward)) {
208    return 2;
209  }
210  std::cout << "Enter lateral speed 0 or ±(0.2 ~ 1.0) m/s: ";
211  std::cin >> lateral;
212  if (!node->set_lateral(lateral)) {
213    return 2;
214  }
215  std::cout << "Enter angular speed 0 or ±(0.1 ~ 1.0) rad/s: ";
216  std::cin >> angular;
217  if (!node->set_angular(angular)) {
218    return 2;
219  }
220
221  RCLCPP_INFO(node->get_logger(), "Setting velocity; moving for 5 seconds");
222
223  node->start_publish();
224
225  auto start_time = node->now();
226  while ((node->now() - start_time).seconds() < 5.0) {
227    rclcpp::spin_some(node);
228    std::this_thread::sleep_for(std::chrono::milliseconds(1));
229  }
230
231  node->clear_velocity();
232  RCLCPP_INFO(node->get_logger(), "5 seconds elapsed; robot stopped");
233
234  rclcpp::spin(node);
235  rclcpp::shutdown();
236  return 0;
237}

6.2.9 关节电机控制

本示例展示了如何使用ROS2和Ruckig库来控制机器人的关节运动。

注意

注意⚠️ :运行本示例前需要在机器人运控计算单元(PC1)使用aima em stop-app mc关闭原生运控模块,获取机器人控制权。注意确保机器人安全

!该示例代码直接对底层电机也就是HAL层进行控制,在运行程序前请检查代码中对关节的安全限位是否与所用机器人相符, 并确保安全!

机器人关节控制示例

这个示例展示了如何使用ROS2和Ruckig库来控制机器人的关节运动。示例实现了以下功能:

  1. 机器人关节模型定义

  2. 基于Ruckig的轨迹插补

  3. 多关节协同控制

  4. 实时位置、速度和加速度控制

依赖项

  • ROS2

  • Ruckig库

  • aimdk_msgs包

构建说明

  1. 将代码放入ROS2工作空间的src目录

  2. 在CMakeLists.txt中添加:

find_package(rclcpp REQUIRED)
find_package(aimdk_msgs REQUIRED)
find_package(ruckig REQUIRED)

add_executable(joint_control_example joint_control_example.cpp)
ament_target_dependencies(joint_control_example
  rclcpp
  aimdk_msgs
  ruckig
)
  1. 在package.xml中添加依赖:

<depend>rclcpp</depend>
<depend>aimdk_msgs</depend>
<depend>ruckig</depend>

示例功能说明

  1. 创建了四个控制器节点,分别控制:

    • 腿部x2(12个关节)

    • 腰部x1(3个关节)

    • 手臂x2(14个关节)

    • 头部x1(2个关节)

  2. 演示功能:

    • 每10秒让指定关节在正负0.5弧度之间摆动

    • 使用Ruckig库实现平滑的运动轨迹

    • 实时发布关节控制命令

自定义使用

  1. 添加新的控制逻辑:

    • 修改SetTargetPosition函数

    • 添加新的控制回调函数

  2. 调整控制频率:

    • 修改control_timer_的周期(当前为2ms)

  1#include <aimdk_msgs/msg/joint_command_array.hpp>
  2#include <aimdk_msgs/msg/joint_state_array.hpp>
  3#include <atomic>
  4#include <cstdlib>
  5#include <memory>
  6#include <rclcpp/rclcpp.hpp>
  7#include <ruckig/ruckig.hpp>
  8#include <signal.h>
  9#include <string>
 10#include <unordered_map>
 11#include <vector>
 12
 13/**
 14 * @brief Global variables and signal handling
 15 */
 16// Global variables to control program state
 17std::atomic<bool> g_running(true);
 18std::atomic<bool> g_emergency_stop(false);
 19
 20// Signal handler function
 21void signal_handler(int) {
 22  g_running = false;
 23  RCLCPP_INFO(rclcpp::get_logger("main"),
 24              "Received termination signal, shutting down...");
 25}
 26
 27/**
 28 * @brief Robot model definition
 29 */
 30enum class JointArea {
 31  HEAD,  // Head joints
 32  ARM,   // Arm joints
 33  WAIST, // Waist joints
 34  LEG,   // Leg joints
 35};
 36
 37/**
 38 * @brief Joint information structure
 39 */
 40struct JointInfo {
 41  std::string name;   // Joint name
 42  double lower_limit; // Joint angle lower limit
 43  double upper_limit; // Joint angle upper limit
 44  double kp;          // Position control gain
 45  double kd;          // Velocity control gain
 46};
 47
 48/**
 49 * @brief Robot model configuration
 50 * Contains parameters for all joints, enabling or disabling specific joints as
 51 * needed
 52 */
 53std::map<JointArea, std::vector<JointInfo>> robot_model = {
 54    {JointArea::LEG,
 55     {
 56         // Left leg joint configuration
 57         {"left_hip_pitch_joint", -2.704, 2.556, 40.0, 4.0},
 58         {"left_hip_roll_joint", -0.235, 2.906, 40.0, 4.0},
 59         {"left_hip_yaw_joint", -1.684, 3.430, 30.0, 3.0},
 60         {"left_knee_joint", 0.0000, 2.4073, 80.0, 8.0},
 61         {"left_ankle_pitch_joint", -0.803, 0.453, 40.0, 4.0},
 62         {"left_ankle_roll_joint", -0.2625, 0.2625, 20.0, 2.0},
 63         // Right leg joint configuration
 64         {"right_hip_pitch_joint", -2.704, 2.556, 40.0, 4.0},
 65         {"right_hip_roll_joint", -2.906, 0.235, 40.0, 4.0},
 66         {"right_hip_yaw_joint", -3.430, 1.684, 30.0, 3.0},
 67         {"right_knee_joint", 0.0000, 2.4073, 80.0, 8.0},
 68         {"right_ankle_pitch_joint", -0.803, 0.453, 40.0, 4.0},
 69         {"right_ankle_roll_joint", -0.2625, 0.2625, 20.0, 2.0},
 70     }},
 71
 72    {JointArea::WAIST,
 73     {
 74         // Waist joint configuration
 75         {"waist_yaw_joint", -3.43, 2.382, 20.0, 4.0},
 76         {"waist_pitch_joint", -0.314, 0.314, 20.0, 4.0},
 77         {"waist_roll_joint", -0.488, 0.488, 20.0, 4.0},
 78     }},
 79    {JointArea::ARM,
 80     {
 81         // Left arm joint configuration
 82         {"left_shoulder_pitch_joint", -3.08, 2.04, 20.0, 2.0},
 83         {"left_shoulder_roll_joint", -0.061, 2.993, 20.0, 2.0},
 84         {"left_shoulder_yaw_joint", -2.556, 2.556, 20.0, 2.0},
 85         {"left_elbow_joint", -2.3556, 0.0, 20.0, 2.0},
 86         {"left_wrist_yaw_joint", -2.556, 2.556, 20.0, 2.0},
 87         {"left_wrist_pitch_joint", -0.558, 0.558, 20.0, 2.0},
 88         {"left_wrist_roll_joint", -1.571, 0.724, 20.0, 2.0},
 89         // Right arm joint configuration
 90         {"right_shoulder_pitch_joint", -3.08, 2.04, 20.0, 2.0},
 91         {"right_shoulder_roll_joint", -2.993, 0.061, 20.0, 2.0},
 92         {"right_shoulder_yaw_joint", -2.556, 2.556, 20.0, 2.0},
 93         {"right_elbow_joint", -2.3556, 0.0000, 20.0, 2.0},
 94         {"right_wrist_yaw_joint", -2.556, 2.556, 20.0, 2.0},
 95         {"right_wrist_pitch_joint", -0.558, 0.558, 20.0, 2.0},
 96         {"right_wrist_roll_joint", -0.724, 1.571, 20.0, 2.0},
 97     }},
 98    {JointArea::HEAD,
 99     {
100         // Head joint configuration
101         {"head_yaw_joint", -0.366, 0.366, 20.0, 2.0},
102         {"head_pitch_joint", -0.3838, 0.3838, 20.0, 2.0},
103     }},
104};
105
106/**
107 * @brief Joint controller node class
108 * @tparam DOFs Degrees of freedom
109 * @tparam Area Joint area
110 */
111template <int DOFs, JointArea Area>
112class JointControllerNode : public rclcpp::Node {
113public:
114  /**
115   * @brief Constructor
116   * @param node_name Node name
117   * @param sub_topic Subscription topic name
118   * @param pub_topic Publication topic name
119   * @param qos QoS configuration
120   */
121  JointControllerNode(std::string node_name, std::string sub_topic,
122                      std::string pub_topic,
123                      rclcpp::QoS qos = rclcpp::SensorDataQoS())
124      : Node(node_name), ruckig(0.002) {
125    joint_info_ = robot_model[Area];
126    if (joint_info_.size() != DOFs) {
127      RCLCPP_ERROR(this->get_logger(), "Joint count mismatch.");
128      exit(1);
129    }
130
131    // Set motion constraints for Ruckig trajectory planner
132    for (int i = 0; i < DOFs; ++i) {
133      input.max_velocity[i] = 1.0;     // Max velocity limit
134      input.max_acceleration[i] = 1.0; // Max acceleration limit
135      input.max_jerk[i] = 25.0; // Max jerk (change of acceleration) limit
136    }
137
138    // Create joint state subscriber
139    sub_ = this->create_subscription<aimdk_msgs::msg::JointStateArray>(
140        sub_topic, qos,
141        std::bind(&JointControllerNode::JointStateCallback, this,
142                  std::placeholders::_1));
143
144    // Create joint command publisher
145    pub_ = this->create_publisher<aimdk_msgs::msg::JointCommandArray>(pub_topic,
146                                                                      qos);
147  }
148
149private:
150  // Ruckig trajectory planner variables
151  ruckig::Ruckig<DOFs> ruckig;          // Trajectory planner instance
152  ruckig::InputParameter<DOFs> input;   // Input parameters
153  ruckig::OutputParameter<DOFs> output; // Output parameters
154  bool ruckig_initialized_ = false;   // Trajectory planner initialization flag
155  std::vector<JointInfo> joint_info_; // Joint information list
156
157  // ROS communication variables
158  rclcpp::Subscription<aimdk_msgs::msg::JointStateArray>::SharedPtr
159      sub_; // State subscriber
160  rclcpp::Publisher<aimdk_msgs::msg::JointCommandArray>::SharedPtr
161      pub_; // Command publisher
162
163  /**
164   * @brief Joint state callback function
165   * @param msg Joint state message
166   */
167  void
168  JointStateCallback(const aimdk_msgs::msg::JointStateArray::SharedPtr msg) {
169    // Initialize trajectory planner on first state reception
170    if (!ruckig_initialized_) {
171      for (int i = 0; i < DOFs; ++i) {
172        input.current_position[i] = msg->joints[i].position;
173        input.current_velocity[i] = msg->joints[i].velocity;
174        input.current_acceleration[i] = 0.0;
175      }
176      ruckig_initialized_ = true;
177      RCLCPP_INFO(this->get_logger(),
178                  "Ruckig trajectory planner initialization complete");
179    }
180  }
181
182public:
183  /**
184   * @brief Set target joint position
185   * @param joint_name Joint name
186   * @param target_position Target position
187   * @return Whether the target position was successfully set
188   */
189  bool SetTargetPosition(std::string joint_name, double target_position) {
190    if (!ruckig_initialized_) {
191      RCLCPP_WARN(this->get_logger(),
192                  "Ruckig trajectory planner not initialized");
193      return false;
194    }
195
196    // Find target joint and set its position
197    int target_joint = -1;
198    for (int i = 0; i < DOFs; ++i) {
199      if (joint_info_[i].name == joint_name) {
200        // Check if target position is within limits
201        if (target_position < joint_info_[i].lower_limit ||
202            target_position > joint_info_[i].upper_limit) {
203          RCLCPP_ERROR(
204              this->get_logger(),
205              "Target position %.3f exceeds limit for joint %s [%.3f, %.3f]",
206              target_position, joint_name.c_str(), joint_info_[i].lower_limit,
207              joint_info_[i].upper_limit);
208          return false;
209        }
210        input.target_position[i] = target_position;
211        input.target_velocity[i] = 0.0;
212        input.target_acceleration[i] = 0.0;
213        target_joint = i;
214      } else {
215        input.target_position[i] = input.current_position[i];
216        input.target_velocity[i] = 0.0;
217        input.target_acceleration[i] = 0.0;
218      }
219    }
220
221    if (target_joint == -1) {
222      RCLCPP_ERROR(this->get_logger(), "Joint %s not found",
223                   joint_name.c_str());
224      return false;
225    }
226
227    // Perform trajectory planning and send command using Ruckig
228    const double tolerance = 1e-6;
229    while (g_running && rclcpp::ok() && !g_emergency_stop) {
230      auto result = ruckig.update(input, output);
231      if (result != ruckig::Result::Working &&
232          result != ruckig::Result::Finished) {
233        RCLCPP_WARN(this->get_logger(), "Trajectory planning failed");
234        break;
235      }
236
237      // Update current state
238      for (int i = 0; i < DOFs; ++i) {
239        input.current_position[i] = output.new_position[i];
240        input.current_velocity[i] = output.new_velocity[i];
241        input.current_acceleration[i] = output.new_acceleration[i];
242      }
243
244      // Check if target position is reached
245      if (std::abs(output.new_position[target_joint] - target_position) <
246          tolerance) {
247        RCLCPP_INFO(this->get_logger(), "Joint %s reached target position",
248                    joint_name.c_str());
249        break;
250      }
251
252      // Create and send joint command
253      aimdk_msgs::msg::JointCommandArray cmd;
254      cmd.joints.resize(DOFs);
255      for (int i = 0; i < DOFs; ++i) {
256        auto &joint = joint_info_[i];
257        cmd.joints[i].name = joint.name;
258        cmd.joints[i].position = output.new_position[i];
259        cmd.joints[i].velocity = output.new_velocity[i];
260        cmd.joints[i].stiffness = joint.kp;
261        cmd.joints[i].damping = joint.kd;
262      }
263      pub_->publish(cmd);
264
265      // Short delay to avoid excessive CPU usage
266      std::this_thread::sleep_for(std::chrono::milliseconds(2));
267    }
268
269    return true;
270  }
271
272  /**
273   * @brief Safely stop all joints
274   */
275  void safe_stop() {
276    if (!ruckig_initialized_) {
277      RCLCPP_WARN(this->get_logger(), "Ruckig trajectory planner not "
278                                      "initialized, cannot perform safe stop");
279      return;
280    }
281
282    RCLCPP_INFO(this->get_logger(), "Performing safe stop...");
283
284    // Set all joint target positions to current positions
285    for (int i = 0; i < DOFs; ++i) {
286      input.target_position[i] = input.current_position[i];
287      input.target_velocity[i] = 0.0;
288      input.target_acceleration[i] = 0.0;
289    }
290
291    // Send final command to ensure joints stop
292    aimdk_msgs::msg::JointCommandArray cmd;
293    cmd.joints.resize(DOFs);
294    for (int i = 0; i < DOFs; ++i) {
295      auto &joint = joint_info_[i];
296      cmd.joints[i].name = joint.name;
297      cmd.joints[i].position = input.current_position[i];
298      cmd.joints[i].velocity = 0.0;
299      cmd.joints[i].stiffness = joint.kp;
300      cmd.joints[i].damping = joint.kd;
301    }
302    pub_->publish(cmd);
303
304    RCLCPP_INFO(this->get_logger(), "Safe stop complete");
305  }
306
307  /**
308   * @brief Emergency stop for all joints
309   */
310  void emergency_stop() {
311    g_emergency_stop = true;
312    safe_stop();
313    RCLCPP_ERROR(this->get_logger(), "Emergency stop triggered");
314  }
315};
316
317/**
318 * @brief Main function
319 */
320int main(int argc, char *argv[]) {
321  rclcpp::init(argc, argv);
322
323  // Set up signal handling
324  signal(SIGINT, signal_handler);
325  signal(SIGTERM, signal_handler);
326
327  try {
328    // Create leg controller node
329    auto leg_node = std::make_shared<JointControllerNode<12, JointArea::LEG>>(
330        "leg_node", "/aima/hal/joint/leg/state", "/aima/hal/joint/leg/command");
331
332    // Create timer node
333    rclcpp::Node::SharedPtr timer_node =
334        rclcpp::Node::make_shared("timer_node");
335    double position = 0.8;
336
337    // Create timer callback function
338    auto timer = timer_node->create_wall_timer(std::chrono::seconds(3), [&]() {
339      if (!g_running || g_emergency_stop)
340        return; // If the program is shutting down or emergency stopped, do not
341                // execute new actions
342      position = -position;
343      position = 1.3 + position;
344      if (!leg_node->SetTargetPosition("left_knee_joint", position)) {
345        RCLCPP_ERROR(rclcpp::get_logger("main"),
346                     "Failed to set target position");
347      }
348    });
349
350    // Create executor
351    rclcpp::executors::MultiThreadedExecutor executor;
352    executor.add_node(leg_node);
353    executor.add_node(timer_node);
354
355    // Main loop
356    while (g_running && rclcpp::ok() && !g_emergency_stop) {
357      executor.spin_once(std::chrono::milliseconds(100));
358    }
359
360    // Safely stop all joints
361    RCLCPP_INFO(rclcpp::get_logger("main"), "Safely stopping all joints...");
362    leg_node->safe_stop();
363
364    // Wait a short time to ensure command transmission is complete
365    std::this_thread::sleep_for(std::chrono::milliseconds(100));
366
367    // Clean up resources
368    RCLCPP_INFO(rclcpp::get_logger("main"), "Cleaning up resources...");
369    leg_node.reset();
370    timer_node.reset();
371
372  } catch (const std::exception &e) {
373    RCLCPP_ERROR(rclcpp::get_logger("main"), "Exception occurred: %s",
374                 e.what());
375    g_emergency_stop = true;
376  } catch (...) {
377    RCLCPP_ERROR(rclcpp::get_logger("main"), "Unknown exception occurred");
378    g_emergency_stop = true;
379  }
380
381  RCLCPP_INFO(rclcpp::get_logger("main"), "Program exited safely");
382  rclcpp::shutdown();
383  return 0;
384}

6.2.10 键盘控制机器人

本示例实现了通过PC的键盘输入控制机器人的前进后退转弯的功能。

通过WASD控制机器人行走方向,增加/减少速度(±0.2 m/s),Q/E增加/减少角速度(±0.1 rad/s),ESC退出程序并释放终端资源,Space立即将速度归零,执行急停

小心

注意:运行本示例前需要先使用手柄,将机器人切入稳定站立模式。(位控站立/走跑模式时按R2+X, 其他模式详见模式流转图进行操作),然后在机器人的终端界面使用:aima em stop-app rc关闭遥控器,防止通道占用。

实现键盘控制前需要注册输入源(该示例已实现注册输入源)
运行前需要安装curse模块:

  sudo apt install libncurses-dev
  1#include "aimdk_msgs/msg/common_request.hpp"
  2#include "aimdk_msgs/msg/common_response.hpp"
  3#include "aimdk_msgs/msg/common_state.hpp"
  4#include "aimdk_msgs/msg/common_task_response.hpp"
  5#include "aimdk_msgs/msg/mc_input_action.hpp"
  6#include "aimdk_msgs/msg/mc_locomotion_velocity.hpp"
  7#include "aimdk_msgs/msg/message_header.hpp"
  8#include "aimdk_msgs/srv/set_mc_input_source.hpp"
  9
 10#include <algorithm>
 11#include <chrono>
 12#include <csignal>
 13#include <curses.h>
 14#include <rclcpp/rclcpp.hpp>
 15
 16using aimdk_msgs::msg::McLocomotionVelocity;
 17using std::placeholders::_1;
 18
 19class KeyboardVelocityController : public rclcpp::Node {
 20public:
 21  KeyboardVelocityController()
 22      : Node("keyboard_velocity_controller"), forward_velocity_(0.0),
 23        lateral_velocity_(0.0), angular_velocity_(0.0), step_(0.2),
 24        angular_step_(0.1) {
 25    pub_ = this->create_publisher<McLocomotionVelocity>(
 26        "/aima/mc/locomotion/velocity", 10);
 27    client_ = this->create_client<aimdk_msgs::srv::SetMcInputSource>(
 28        "/aimdk_5Fmsgs/srv/SetMcInputSource");
 29    // Register input source
 30    if (!register_input_source()) {
 31      RCLCPP_ERROR(this->get_logger(),
 32                   "Input source registration failed, exiting");
 33      throw std::runtime_error("Input source registration failed");
 34    }
 35    // Initialize ncurses
 36    initscr();
 37    cbreak();
 38    noecho();
 39    keypad(stdscr, TRUE);
 40    nodelay(stdscr, TRUE);
 41
 42    timer_ = this->create_wall_timer(
 43        std::chrono::milliseconds(50),
 44        std::bind(&KeyboardVelocityController::checkKeyAndPublish, this));
 45
 46    RCLCPP_INFO(this->get_logger(),
 47                "Control started: W/S Forward/Backward | A/D Strafe Left/Right "
 48                "| Q/E Turn Left/Right | Space Stop | ESC Exit");
 49  }
 50
 51  ~KeyboardVelocityController() {
 52    endwin(); // Restore terminal
 53  }
 54
 55private:
 56  rclcpp::Publisher<McLocomotionVelocity>::SharedPtr pub_;
 57  rclcpp::Client<aimdk_msgs::srv::SetMcInputSource>::SharedPtr client_;
 58  rclcpp::TimerBase::SharedPtr timer_;
 59
 60  float forward_velocity_, lateral_velocity_, angular_velocity_;
 61  const float step_, angular_step_;
 62
 63  bool register_input_source() {
 64    const std::chrono::seconds srv_timeout(8);
 65    auto start_time = std::chrono::steady_clock::now();
 66    while (!client_->wait_for_service(std::chrono::seconds(2))) {
 67      if (std::chrono::steady_clock::now() - start_time > srv_timeout) {
 68        RCLCPP_ERROR(this->get_logger(), "Waiting for service timed out");
 69        return false;
 70      }
 71      RCLCPP_INFO(this->get_logger(), "Waiting for input source service...");
 72    }
 73
 74    auto request =
 75        std::make_shared<aimdk_msgs::srv::SetMcInputSource::Request>();
 76    request->action.value = 1001;
 77    request->input_source.name = "node";
 78    request->input_source.priority = 40;
 79    request->input_source.timeout = 1000;
 80
 81    auto timeout = std::chrono::milliseconds(250);
 82
 83    for (int i = 0; i < 8; i++) {
 84      request->request.header.stamp = this->now();
 85      auto future = client_->async_send_request(request);
 86      auto retcode = rclcpp::spin_until_future_complete(
 87          this->get_node_base_interface(), future, timeout);
 88      if (retcode != rclcpp::FutureReturnCode::SUCCESS) {
 89        // retry as remote peer is NOT handled well by ROS
 90        RCLCPP_INFO(this->get_logger(),
 91                    "trying to register input source... [%d]", i);
 92        continue;
 93      }
 94      // future.done
 95      auto response = future.get();
 96      int state = response->response.state.value;
 97      RCLCPP_INFO(this->get_logger(),
 98                  "Set input source succeeded: state=%d, task_id=%lu", state,
 99                  response->response.task_id);
100      return true;
101    }
102    RCLCPP_ERROR(this->get_logger(), "Service call failed or timed out");
103    return false;
104  }
105
106  void checkKeyAndPublish() {
107    int ch = getch(); // non-blocking read
108
109    switch (ch) {
110    case ' ': // Space key
111      forward_velocity_ = 0.0;
112      lateral_velocity_ = 0.0;
113      angular_velocity_ = 0.0;
114      break;
115    case 'w':
116      forward_velocity_ = std::min(forward_velocity_ + step_, 1.0f);
117      break;
118    case 's':
119      forward_velocity_ = std::max(forward_velocity_ - step_, -1.0f);
120      break;
121    case 'a':
122      lateral_velocity_ = std::min(lateral_velocity_ + step_, 1.0f);
123      break;
124    case 'd':
125      lateral_velocity_ = std::max(lateral_velocity_ - step_, -1.0f);
126      break;
127    case 'q':
128      angular_velocity_ = std::min(angular_velocity_ + angular_step_, 1.0f);
129      break;
130    case 'e':
131      angular_velocity_ = std::max(angular_velocity_ - angular_step_, -1.0f);
132      break;
133    case 27: // ESC Key
134      RCLCPP_INFO(this->get_logger(), "Exiting control");
135      rclcpp::shutdown();
136      return;
137    }
138
139    auto msg = std::make_unique<McLocomotionVelocity>();
140    msg->header = aimdk_msgs::msg::MessageHeader();
141    msg->header.stamp = this->now();
142    msg->source = "node";
143    msg->forward_velocity = forward_velocity_;
144    msg->lateral_velocity = lateral_velocity_;
145    msg->angular_velocity = angular_velocity_;
146
147    float fwd = forward_velocity_;
148    float lat = lateral_velocity_;
149    float ang = angular_velocity_;
150
151    pub_->publish(std::move(msg));
152
153    // Screen Output
154    clear();
155    mvprintw(0, 0,
156             "W/S: Forward/Backward | A/D: Left/Right Strafe | Q/E: Turn "
157             "Left/Right | Space: Stop | ESC: Exit");
158    mvprintw(2, 0,
159             "Speed Status: Forward: %.2f m/s | Lateral: %.2f m/s | Angular: "
160             "%.2f rad/s",
161             fwd, lat, ang);
162    refresh();
163  }
164};
165
166int main(int argc, char *argv[]) {
167  rclcpp::init(argc, argv);
168  try {
169    auto node = std::make_shared<KeyboardVelocityController>();
170    rclcpp::spin(node);
171  } catch (const std::exception &e) {
172    RCLCPP_FATAL(rclcpp::get_logger("main"),
173                 "Program exited with exception: %s", e.what());
174  }
175  rclcpp::shutdown();
176  return 0;
177}

6.2.11 拍照

该示例中用到了take_photo,在运行节点程序前,修改需要拍照的相机话题,启动节点程序后,会创建/images/目录,将当前帧图像保存在这个目录下。

 1#include <chrono>
 2#include <cv_bridge/cv_bridge.h>
 3#include <filesystem>
 4#include <opencv2/opencv.hpp>
 5#include <rclcpp/rclcpp.hpp>
 6#include <sensor_msgs/msg/image.hpp>
 7#include <string>
 8
 9class SaveOneRaw : public rclcpp::Node {
10public:
11  SaveOneRaw() : Node("save_one_image"), saved_(false) {
12    topic_ = declare_parameter<std::string>(
13        "image_topic", "/aima/hal/sensor/stereo_head_front_left/rgb_image");
14
15    std::filesystem::create_directories("images");
16
17    auto qos = rclcpp::SensorDataQoS(); // BestEffort/Volatile
18    sub_ = create_subscription<sensor_msgs::msg::Image>(
19        topic_, qos, std::bind(&SaveOneRaw::cb, this, std::placeholders::_1));
20
21    RCLCPP_INFO(get_logger(), "Subscribing (raw): %s", topic_.c_str());
22  }
23
24private:
25  void cb(const sensor_msgs::msg::Image::SharedPtr msg) {
26    if (saved_)
27      return;
28
29    try {
30      // Obtain the Mat without copying by not specifying encoding
31      cv_bridge::CvImageConstPtr cvp = cv_bridge::toCvShare(msg);
32      cv::Mat img = cvp->image;
33
34      // Convert to BGR for uniform saving
35      if (msg->encoding == "rgb8") {
36        cv::cvtColor(img, img, cv::COLOR_RGB2BGR);
37      } else if (msg->encoding == "mono8") {
38        cv::cvtColor(img, img, cv::COLOR_GRAY2BGR);
39      } // bgr8 Use this directly; add more branches as needed to support
40        // additional encodings.
41
42      auto now = std::chrono::system_clock::now();
43      auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
44                    now.time_since_epoch())
45                    .count();
46      std::string path = "images/frame_" + std::to_string(ms) + ".png";
47
48      if (cv::imwrite(path, img)) {
49        RCLCPP_INFO(get_logger(), "Saved: %s  (%dx%d)", path.c_str(), img.cols,
50                    img.rows);
51        saved_ = true;
52        rclcpp::shutdown();
53      } else {
54        RCLCPP_ERROR(get_logger(), "cv::imwrite failed: %s", path.c_str());
55      }
56    } catch (const std::exception &e) {
57      RCLCPP_ERROR(get_logger(), "raw decode failed: %s", e.what());
58      // Do not set the saved flag; wait for the next frame
59    }
60  }
61
62  std::string topic_;
63  bool saved_;
64  rclcpp::Subscription<sensor_msgs::msg::Image>::SharedPtr sub_;
65};
66
67int main(int argc, char **argv) {
68  rclcpp::init(argc, argv);
69  rclcpp::spin(std::make_shared<SaveOneRaw>());
70  return 0;
71}

6.2.12 相机推流示例集

该示例集提供了多种相机数据订阅和处理功能,支持深度相机、双目相机和单目相机的数据流订阅。
这些相机数据订阅 example 并没有实际的业务用途, 仅提供相机数据基础信息的打印;若您比较熟悉 ros2 使用,会发现 ros2 topic echo + ros2 topic hz 也能够实现 example 提供的功能。 您可以选择快速查阅 SDK 接口手册中话题列表直接快速进入自己模块的开发,也可以使用相机 example 作为脚手架加入自己的业务逻辑。 我们发布的传感器数据均为未经预处理(去畸变等)的原始数据,如果您需要查询传感器的详细信息(如分辨率、焦距等),请关注相应的内参(camera_info)话题。

深度相机数据订阅

该示例中用到了echo_camera_rgbd,通过订阅/aima/hal/sensor/rgbd_head_front/话题来接收机器人的深度相机数据,支持深度点云、深度图、RGB图、压缩RGB图和相机内参等多种数据类型。

功能特点:

  • 支持多种数据类型订阅(深度点云、深度图、RGB图、压缩图、相机内参)

  • 实时FPS统计和数据显示

  • 支持RGB图视频录制功能

  • 可配置的topic类型选择

支持的数据类型:

  • depth_pointcloud: 深度点云数据 (sensor_msgs/PointCloud2)

  • depth_image: 深度图像 (sensor_msgs/Image)

  • rgb_image: RGB图像 (sensor_msgs/Image)

  • rgb_image_compressed: 压缩RGB图像 (sensor_msgs/CompressedImage)

  • camera_info: 相机内参 (sensor_msgs/CameraInfo)

  1#include <deque>
  2#include <iomanip>
  3#include <memory>
  4#include <rclcpp/rclcpp.hpp>
  5#include <sensor_msgs/msg/camera_info.hpp>
  6#include <sensor_msgs/msg/compressed_image.hpp>
  7#include <sensor_msgs/msg/image.hpp>
  8#include <sensor_msgs/msg/point_cloud2.hpp>
  9#include <sstream>
 10#include <string>
 11#include <vector>
 12
 13// OpenCV headers for image/video writing
 14#include <cv_bridge/cv_bridge.h>
 15#include <opencv2/opencv.hpp>
 16
 17/**
 18 * @brief Example of subscribing to multiple topics for the head depth camera
 19 *
 20 * You can select which topic type to subscribe to via the startup argument
 21 * --ros-args -p topic_type:=<type>:
 22 *   - depth_pointcloud: Depth point cloud (sensor_msgs/PointCloud2)
 23 *   - depth_image: Depth image (sensor_msgs/Image)
 24 *   - rgb_image: RGB image (sensor_msgs/Image)
 25 *   - rgb_image_compressed: RGB compressed image (sensor_msgs/CompressedImage)
 26 *   - rgb_camera_info: Camera intrinsic parameters (sensor_msgs/CameraInfo)
 27 *   - depth_camera_info: Camera intrinsic parameters (sensor_msgs/CameraInfo)
 28 *
 29 * Examples:
 30 *   ros2 run examples echo_camera_rgbd --ros-args -p
 31 * topic_type:=depth_pointcloud ros2 run examples echo_camera_rgbd --ros-args -p
 32 * topic_type:=rgb_image ros2 run examples echo_camera_rgbd --ros-args -p
 33 * topic_type:=rgb_camera_info
 34 *
 35 * topic_type defaults to "rgb_image"
 36 *
 37 * See individual callbacks for more detailed comments
 38 */
 39class CameraTopicEcho : public rclcpp::Node {
 40public:
 41  CameraTopicEcho() : Node("camera_topic_echo") {
 42    // Select which topic type to subscribe to
 43    topic_type_ = declare_parameter<std::string>("topic_type", "rgb_image");
 44    dump_video_path_ = declare_parameter<std::string>("dump_video_path", "");
 45
 46    // Subscribed topics and their message layouts
 47    // 1. /aima/hal/sensor/rgbd_head_front/depth_pointcloud
 48    //    - topic_type: depth_pointcloud
 49    //    - message type: sensor_msgs::msg::PointCloud2
 50    //    - frame_id: rgbd_head_front
 51    //    - child_frame_id: /
 52    //    - contents: depth point cloud
 53    // 2. /aima/hal/sensor/rgbd_head_front/depth_image
 54    //    - topic_type: depth_image
 55    //    - message type: sensor_msgs::msg::Image
 56    //    - frame_id: rgbd_head_front
 57    //    - contents: depth image
 58    // 3. /aima/hal/sensor/rgbd_head_front/rgb_image
 59    //    - topic_type: rgb_image
 60    //    - message type: sensor_msgs::msg::Image
 61    //    - frame_id: rgbd_head_front
 62    //    - contents: RGB image
 63    // 4. /aima/hal/sensor/rgbd_head_front/rgb_image/compressed
 64    //    - topic_type: rgb_image_compressed
 65    //    - message type: sensor_msgs::msg::CompressedImage
 66    //    - frame_id: rgbd_head_front
 67    //    - contents: RGB compressed image
 68    // 5. /aima/hal/sensor/rgbd_head_front/rgb_camera_info
 69    //    - topic_type: camera_info
 70    //    - message type: sensor_msgs::msg::CameraInfo
 71    //    - frame_id: rgbd_head_front
 72    //    - contents: RGB camera intrinsic parameters
 73    // 6. /aima/hal/sensor/rgbd_head_front/depth_camera_info
 74    //    - topic_type: camera_info
 75    //    - message type: sensor_msgs::msg::CameraInfo
 76    //    - frame_id: rgbd_head_front
 77    //    - contents: RGB camera intrinsic parameters
 78
 79    auto qos = rclcpp::SensorDataQoS();
 80
 81    // Enable depth pointcloud subscription
 82    if (topic_type_ == "depth_pointcloud") {
 83      topic_name_ = "/aima/hal/sensor/rgbd_head_front/depth_pointcloud";
 84      sub_pointcloud_ = create_subscription<sensor_msgs::msg::PointCloud2>(
 85          topic_name_, qos,
 86          std::bind(&CameraTopicEcho::cb_pointcloud, this,
 87                    std::placeholders::_1));
 88      RCLCPP_INFO(get_logger(), "✅ Subscribing PointCloud2: %s",
 89                  topic_name_.c_str());
 90
 91      // Enable depth image subscription
 92    } else if (topic_type_ == "depth_image") {
 93      topic_name_ = "/aima/hal/sensor/rgbd_head_front/depth_image";
 94      sub_image_ = create_subscription<sensor_msgs::msg::Image>(
 95          topic_name_, qos,
 96          std::bind(&CameraTopicEcho::cb_image, this, std::placeholders::_1));
 97      RCLCPP_INFO(get_logger(), "✅ Subscribing Depth Image: %s",
 98                  topic_name_.c_str());
 99
100      // Enable RGB image subscription
101    } else if (topic_type_ == "rgb_image") {
102      topic_name_ = "/aima/hal/sensor/rgbd_head_front/rgb_image";
103      sub_image_ = create_subscription<sensor_msgs::msg::Image>(
104          topic_name_, qos,
105          std::bind(&CameraTopicEcho::cb_image, this, std::placeholders::_1));
106      RCLCPP_INFO(get_logger(), "✅ Subscribing RGB Image: %s",
107                  topic_name_.c_str());
108      if (!dump_video_path_.empty()) {
109        RCLCPP_INFO(get_logger(), "📝 Will dump received images to video: %s",
110                    dump_video_path_.c_str());
111      }
112
113      // Enable RGB compressed image subscription
114    } else if (topic_type_ == "rgb_image_compressed") {
115      topic_name_ = "/aima/hal/sensor/rgbd_head_front/rgb_image/compressed";
116      sub_compressed_ = create_subscription<sensor_msgs::msg::CompressedImage>(
117          topic_name_, qos,
118          std::bind(&CameraTopicEcho::cb_compressed, this,
119                    std::placeholders::_1));
120      RCLCPP_INFO(get_logger(), "✅ Subscribing CompressedImage: %s",
121                  topic_name_.c_str());
122
123      // Enable rgb camera info subscription
124    } else if (topic_type_ == "rgb_camera_info") {
125      topic_name_ = "/aima/hal/sensor/rgbd_head_front/rgb_camera_info";
126      // RGB-D CameraInfo subscriptions is different with other cameras.
127      // The messages arrive in about 10Hz and SensorDataQoS is enough.
128      sub_camerainfo_ = create_subscription<sensor_msgs::msg::CameraInfo>(
129          topic_name_, qos,
130          std::bind(&CameraTopicEcho::cb_camerainfo, this,
131                    std::placeholders::_1));
132      RCLCPP_INFO(get_logger(), "✅ Subscribing RGB CameraInfo: %s",
133                  topic_name_.c_str());
134
135      // Enable depth camera info subscription
136    } else if (topic_type_ == "depth_camera_info") {
137      topic_name_ = "/aima/hal/sensor/rgbd_head_front/depth_camera_info";
138      // RGB-D CameraInfo subscriptions is different with other cameras.
139      // The messages arrive in about 10Hz and SensorDataQoS is enough.
140      sub_camerainfo_ = create_subscription<sensor_msgs::msg::CameraInfo>(
141          topic_name_, qos,
142          std::bind(&CameraTopicEcho::cb_camerainfo, this,
143                    std::placeholders::_1));
144      RCLCPP_INFO(get_logger(), "✅ Subscribing Depth CameraInfo: %s",
145                  topic_name_.c_str());
146
147      // Unknown topic_type error
148    } else {
149      RCLCPP_ERROR(get_logger(), "Unknown topic_type: %s", topic_type_.c_str());
150      throw std::runtime_error("Unknown topic_type");
151    }
152  }
153
154  ~CameraTopicEcho() override {
155    if (video_writer_.isOpened()) {
156      video_writer_.release();
157      RCLCPP_INFO(get_logger(), "Video file closed.");
158    }
159  }
160
161private:
162  // PointCloud2 callback
163  void cb_pointcloud(const sensor_msgs::msg::PointCloud2::SharedPtr msg) {
164    // Update arrivals (for FPS calculation)
165    update_arrivals();
166
167    // Print point cloud basic info
168    if (should_print()) {
169      std::ostringstream oss;
170      oss << "🌫️ PointCloud2 received\n"
171          << "  • frame_id:        " << msg->header.frame_id << "\n"
172          << "  • stamp (sec):     "
173          << rclcpp::Time(msg->header.stamp).seconds() << "\n"
174          << "  • width x height:  " << msg->width << " x " << msg->height
175          << "\n"
176          << "  • point_step:      " << msg->point_step << "\n"
177          << "  • row_step:        " << msg->row_step << "\n"
178          << "  • fields:          ";
179      for (const auto &f : msg->fields)
180        oss << f.name << "(" << (int)f.datatype << ") ";
181      oss << "\n  • is_bigendian:    " << msg->is_bigendian
182          << "\n  • is_dense:        " << msg->is_dense
183          << "\n  • data size:       " << msg->data.size()
184          << "\n  • recv FPS (1s):   " << get_fps();
185      RCLCPP_INFO(get_logger(), "%s", oss.str().c_str());
186    }
187  }
188
189  // Image callback (depth/RGB image)
190  void cb_image(const sensor_msgs::msg::Image::SharedPtr msg) {
191    update_arrivals();
192
193    if (should_print()) {
194      RCLCPP_INFO(get_logger(),
195                  "📸 %s received\n"
196                  "  • frame_id:        %s\n"
197                  "  • stamp (sec):     %.6f\n"
198                  "  • encoding:        %s\n"
199                  "  • size (WxH):      %u x %u\n"
200                  "  • step (bytes/row):%u\n"
201                  "  • is_bigendian:    %u\n"
202                  "  • recv FPS (1s):   %.1f",
203                  topic_type_.c_str(), msg->header.frame_id.c_str(),
204                  rclcpp::Time(msg->header.stamp).seconds(),
205                  msg->encoding.c_str(), msg->width, msg->height, msg->step,
206                  msg->is_bigendian, get_fps());
207    }
208
209    // Video dump is supported only for RGB images
210    if (topic_type_ == "rgb_image" && !dump_video_path_.empty()) {
211      dump_image_to_video(msg);
212    }
213  }
214
215  // CompressedImage callback
216  void cb_compressed(const sensor_msgs::msg::CompressedImage::SharedPtr msg) {
217    update_arrivals();
218
219    if (should_print()) {
220      RCLCPP_INFO(get_logger(),
221                  "🗜️  CompressedImage received\n"
222                  "  • frame_id:        %s\n"
223                  "  • stamp (sec):     %.6f\n"
224                  "  • format:          %s\n"
225                  "  • data size:       %zu\n"
226                  "  • recv FPS (1s):   %.1f",
227                  msg->header.frame_id.c_str(),
228                  rclcpp::Time(msg->header.stamp).seconds(),
229                  msg->format.c_str(), msg->data.size(), get_fps());
230    }
231  }
232
233  // CameraInfo callback (camera intrinsic parameters)
234  void cb_camerainfo(const sensor_msgs::msg::CameraInfo::SharedPtr msg) {
235    // CameraInfo is typically published once; print it once
236    std::ostringstream oss;
237    oss << "📷 " << topic_type_ << " received\n"
238        << "  • frame_id:        " << msg->header.frame_id << "\n"
239        << "  • stamp (sec):     " << rclcpp::Time(msg->header.stamp).seconds()
240        << "\n"
241        << "  • width x height:  " << msg->width << " x " << msg->height << "\n"
242        << "  • distortion_model:" << msg->distortion_model << "\n"
243        << "  • D: [";
244    for (size_t i = 0; i < msg->d.size(); ++i) {
245      oss << msg->d[i];
246      if (i + 1 < msg->d.size())
247        oss << ", ";
248    }
249    oss << "]\n  • K: [";
250    for (int i = 0; i < 9; ++i) {
251      oss << msg->k[i];
252      if (i + 1 < 9)
253        oss << ", ";
254    }
255    oss << "]\n  • R: [";
256    for (int i = 0; i < 9; ++i) {
257      oss << msg->r[i];
258      if (i + 1 < 9)
259        oss << ", ";
260    }
261    oss << "]\n  • P: [";
262    for (int i = 0; i < 12; ++i) {
263      oss << msg->p[i];
264      if (i + 1 < 12)
265        oss << ", ";
266    }
267    oss << "]\n"
268        << "  • binning_x: " << msg->binning_x << "\n"
269        << "  • binning_y: " << msg->binning_y << "\n"
270        << "  • roi: { x_offset: " << msg->roi.x_offset
271        << ", y_offset: " << msg->roi.y_offset
272        << ", height: " << msg->roi.height << ", width: " << msg->roi.width
273        << ", do_rectify: " << (msg->roi.do_rectify ? "true" : "false") << " }";
274    RCLCPP_INFO(get_logger(), "%s", oss.str().c_str());
275  }
276
277  // Track arrival timestamps to compute FPS
278  void update_arrivals() {
279    const rclcpp::Time now = this->get_clock()->now();
280    arrivals_.push_back(now);
281    while (!arrivals_.empty() && (now - arrivals_.front()).seconds() > 1.0) {
282      arrivals_.pop_front();
283    }
284  }
285  double get_fps() const { return static_cast<double>(arrivals_.size()); }
286
287  // Control printing frequency
288  bool should_print() {
289    const rclcpp::Time now = this->get_clock()->now();
290    if ((now - last_print_).seconds() >= 1.0) {
291      last_print_ = now;
292      return true;
293    }
294    return false;
295  }
296
297  // Dump received images to a video file (RGB images only)
298  void dump_image_to_video(const sensor_msgs::msg::Image::SharedPtr &msg) {
299    cv::Mat image;
300    try {
301      // Obtain the Mat without copying by not specifying encoding
302      cv_bridge::CvImageConstPtr cvp = cv_bridge::toCvShare(msg);
303      image = cvp->image;
304      // Convert to BGR for uniform saving
305      if (msg->encoding == "rgb8") {
306        cv::cvtColor(image, image, cv::COLOR_RGB2BGR);
307      } else {
308        RCLCPP_WARN(get_logger(), "image encoding not expected: %s",
309                    msg->encoding.c_str());
310        return;
311      }
312    } catch (const std::exception &e) {
313      RCLCPP_WARN(get_logger(), "cv_bridge exception: %s", e.what());
314      return;
315    }
316
317    // Initialize VideoWriter
318    if (!video_writer_.isOpened()) {
319      int fourcc = cv::VideoWriter::fourcc('M', 'J', 'P', 'G');
320      double fps = std::max(1.0, get_fps());
321      bool ok = video_writer_.open(dump_video_path_, fourcc, fps,
322                                   cv::Size(image.cols, image.rows), true);
323      if (!ok) {
324        RCLCPP_ERROR(get_logger(), "Failed to open video file: %s",
325                     dump_video_path_.c_str());
326        dump_video_path_.clear(); // stop trying
327        return;
328      }
329      RCLCPP_INFO(get_logger(), "VideoWriter started: %s, size=%dx%d, fps=%.1f",
330                  dump_video_path_.c_str(), image.cols, image.rows, fps);
331    }
332    video_writer_.write(image);
333  }
334
335  // Member variables
336  std::string topic_type_;
337  std::string topic_name_;
338  std::string dump_video_path_;
339
340  // Subscriptions
341  rclcpp::Subscription<sensor_msgs::msg::Image>::SharedPtr sub_image_;
342  rclcpp::Subscription<sensor_msgs::msg::CompressedImage>::SharedPtr
343      sub_compressed_;
344  rclcpp::Subscription<sensor_msgs::msg::CameraInfo>::SharedPtr sub_camerainfo_;
345  rclcpp::Subscription<sensor_msgs::msg::PointCloud2>::SharedPtr
346      sub_pointcloud_;
347
348  // FPS statistics
349  rclcpp::Time last_print_{0, 0, RCL_ROS_TIME};
350  std::deque<rclcpp::Time> arrivals_;
351
352  // Video writer
353  cv::VideoWriter video_writer_;
354};
355
356int main(int argc, char **argv) {
357  rclcpp::init(argc, argv);
358  auto node = std::make_shared<CameraTopicEcho>();
359  rclcpp::spin(node);
360  rclcpp::shutdown();
361  return 0;
362}

使用说明:

  1. 订阅深度点云数据:

    ros2 run examples echo_camera_rgbd --ros-args -p topic_type:=depth_pointcloud
    
  2. 订阅RGB图像数据:

    ros2 run examples echo_camera_rgbd --ros-args -p topic_type:=rgb_image
    
  3. 订阅相机内参:

    ros2 run examples echo_camera_rgbd --ros-args -p topic_type:=rgb_camera_info
    ros2 run examples echo_camera_rgbd --ros-args -p topic_type:=depth_camera_info
    
  4. 录制RGB视频:

    # dump_video_path的值可改为其他路径, 注意提前创建该文件所在目录才能保存
    ros2 run examples echo_camera_rgbd --ros-args -p topic_type:=rgb_image -p dump_video_path:=$PWD/output.avi
    

双目相机数据订阅

该示例中用到了echo_camera_stereo,通过订阅/aima/hal/sensor/stereo_head_front_*/话题来接收机器人的双目相机数据,支持左右相机的RGB图、压缩图和相机内参数据。

功能特点:

  • 支持左右相机独立数据订阅

  • 实时FPS统计和数据显示

  • 支持RGB图视频录制功能

  • 可配置的相机选择(左/右)

支持的数据类型:

  • left_rgb_image: 左相机RGB图像 (sensor_msgs/Image)

  • left_rgb_image_compressed: 左相机压缩RGB图像 (sensor_msgs/CompressedImage)

  • left_camera_info: 左相机内参 (sensor_msgs/CameraInfo)

  • right_rgb_image: 右相机RGB图像 (sensor_msgs/Image)

  • right_rgb_image_compressed: 右相机压缩RGB图像 (sensor_msgs/CompressedImage)

  • right_camera_info: 右相机内参 (sensor_msgs/CameraInfo)

  1#include <deque>
  2#include <iomanip>
  3#include <memory>
  4#include <rclcpp/rclcpp.hpp>
  5#include <sensor_msgs/msg/camera_info.hpp>
  6#include <sensor_msgs/msg/compressed_image.hpp>
  7#include <sensor_msgs/msg/image.hpp>
  8#include <sstream>
  9#include <string>
 10#include <vector>
 11
 12// OpenCV headers for image/video writing
 13#include <cv_bridge/cv_bridge.h>
 14#include <opencv2/opencv.hpp>
 15
 16/**
 17 * @brief Example of subscribing to multiple topics for the stereo head camera
 18 *
 19 * You can select which topic type to subscribe to via the startup argument
 20 * --ros-args -p topic_type:=<type>:
 21 *   - left_rgb_image: left camera RGB image (sensor_msgs/Image)
 22 *   - left_rgb_image_compressed: left camera RGB compressed image
 23 * (sensor_msgs/CompressedImage)
 24 *   - left_camera_info: left camera intrinsic parameters
 25 * (sensor_msgs/CameraInfo)
 26 *   - right_rgb_image: right camera RGB image (sensor_msgs/Image)
 27 *   - right_rgb_image_compressed: right camera RGB compressed image
 28 * (sensor_msgs/CompressedImage)
 29 *   - right_camera_info: right camera intrinsic parameters
 30 * (sensor_msgs/CameraInfo)
 31 *
 32 * Examples:
 33 *   ros2 run examples echo_camera_stereo --ros-args -p
 34 * topic_type:=left_rgb_image ros2 run examples echo_camera_stereo --ros-args -p
 35 * topic_type:=right_rgb_image ros2 run examples echo_camera_stereo --ros-args
 36 * -p topic_type:=left_camera_info
 37 *
 38 * topic_type defaults to "left_rgb_image"
 39 *
 40 * See individual callbacks for more detailed comments
 41 */
 42class StereoCameraTopicEcho : public rclcpp::Node {
 43public:
 44  StereoCameraTopicEcho() : Node("stereo_camera_topic_echo") {
 45    // Select which topic type to subscribe to
 46    topic_type_ =
 47        declare_parameter<std::string>("topic_type", "left_rgb_image");
 48    dump_video_path_ = declare_parameter<std::string>("dump_video_path", "");
 49
 50    // Subscribed topics and their message layouts
 51    // 1. /aima/hal/sensor/stereo_head_front_left/rgb_image
 52    //    - topic_type: left_rgb_image
 53    //    - message type: sensor_msgs::msg::Image
 54    //    - frame_id: stereo_head_front
 55    //    - child_frame_id: /
 56    //    - contents: left camera raw image
 57    // 2. /aima/hal/sensor/stereo_head_front_left/rgb_image/compressed
 58    //    - topic_type: left_rgb_image_compressed
 59    //    - message type: sensor_msgs::msg::CompressedImage
 60    //    - frame_id: stereo_head_front
 61    //    - contents: left camera compressed image
 62    // 3. /aima/hal/sensor/stereo_head_front_left/camera_info
 63    //    - topic_type: left_camera_info
 64    //    - message type: sensor_msgs::msg::CameraInfo
 65    //    - frame_id: stereo_head_front
 66    //    - contents: left camera intrinsic parameters
 67    // 4. /aima/hal/sensor/stereo_head_front_right/rgb_image
 68    //    - topic_type: right_rgb_image
 69    //    - message type: sensor_msgs::msg::Image
 70    //    - frame_id: stereo_head_front_right
 71    //    - child_frame_id: /
 72    //    - contents: right camera raw image
 73    // 5. /aima/hal/sensor/stereo_head_front_right/rgb_image/compressed
 74    //    - topic_type: right_rgb_image_compressed
 75    //    - message type: sensor_msgs::msg::CompressedImage
 76    //    - frame_id: stereo_head_front_right
 77    //    - contents: right camera compressed image
 78    // 6. /aima/hal/sensor/stereo_head_front_right/camera_info
 79    //    - topic_type: right_camera_info
 80    //    - message type: sensor_msgs::msg::CameraInfo
 81    //    - frame_id: stereo_head_front_right
 82    //    - contents: right camera intrinsic parameters
 83
 84    // Set QoS parameters - use SensorData QoS
 85    auto qos = rclcpp::SensorDataQoS();
 86
 87    // Enable left camera RGB image subscription
 88    if (topic_type_ == "left_rgb_image") {
 89      topic_name_ = "/aima/hal/sensor/stereo_head_front_left/rgb_image";
 90      sub_image_ = create_subscription<sensor_msgs::msg::Image>(
 91          topic_name_, qos,
 92          std::bind(&StereoCameraTopicEcho::cb_image, this,
 93                    std::placeholders::_1));
 94      RCLCPP_INFO(get_logger(), "✅ Subscribing Left RGB Image: %s",
 95                  topic_name_.c_str());
 96      if (!dump_video_path_.empty()) {
 97        RCLCPP_INFO(get_logger(), "📝 Will dump received images to video: %s",
 98                    dump_video_path_.c_str());
 99      }
100
101      // Enable left camera RGB compressed image subscription
102    } else if (topic_type_ == "left_rgb_image_compressed") {
103      topic_name_ =
104          "/aima/hal/sensor/stereo_head_front_left/rgb_image/compressed";
105      sub_compressed_ = create_subscription<sensor_msgs::msg::CompressedImage>(
106          topic_name_, qos,
107          std::bind(&StereoCameraTopicEcho::cb_compressed, this,
108                    std::placeholders::_1));
109      RCLCPP_INFO(get_logger(), "✅ Subscribing Left CompressedImage: %s",
110                  topic_name_.c_str());
111
112      // Enable left camera info subscription
113    } else if (topic_type_ == "left_camera_info") {
114
115      topic_name_ = "/aima/hal/sensor/stereo_head_front_left/camera_info";
116      // CameraInfo subscriptions must use reliable + transient_local
117      // QoS in order to receive latched/history messages (even if only one
118      // message was published). Here we use keep_last(1) + reliable
119      // + transient_local.
120      sub_camerainfo_ = create_subscription<sensor_msgs::msg::CameraInfo>(
121          topic_name_,
122          rclcpp::QoS(rclcpp::KeepLast(1)).reliable().transient_local(),
123          std::bind(&StereoCameraTopicEcho::cb_camerainfo, this,
124                    std::placeholders::_1));
125      RCLCPP_INFO(get_logger(),
126                  "✅ Subscribing Left CameraInfo (with transient_local): %s",
127                  topic_name_.c_str());
128
129      // Enable right camera RGB image subscription
130    } else if (topic_type_ == "right_rgb_image") {
131      topic_name_ = "/aima/hal/sensor/stereo_head_front_right/rgb_image";
132      sub_image_ = create_subscription<sensor_msgs::msg::Image>(
133          topic_name_, qos,
134          std::bind(&StereoCameraTopicEcho::cb_image, this,
135                    std::placeholders::_1));
136      RCLCPP_INFO(get_logger(), "✅ Subscribing Right RGB Image: %s",
137                  topic_name_.c_str());
138      if (!dump_video_path_.empty()) {
139        RCLCPP_INFO(get_logger(), "📝 Will dump received images to video: %s",
140                    dump_video_path_.c_str());
141      }
142
143      // Enable right camera RGB compressed image subscription
144    } else if (topic_type_ == "right_rgb_image_compressed") {
145      topic_name_ =
146          "/aima/hal/sensor/stereo_head_front_right/rgb_image/compressed";
147      sub_compressed_ = create_subscription<sensor_msgs::msg::CompressedImage>(
148          topic_name_, qos,
149          std::bind(&StereoCameraTopicEcho::cb_compressed, this,
150                    std::placeholders::_1));
151      RCLCPP_INFO(get_logger(), "✅ Subscribing Right CompressedImage: %s",
152                  topic_name_.c_str());
153
154      // Enable right camera info subscription
155    } else if (topic_type_ == "right_camera_info") {
156      topic_name_ = "/aima/hal/sensor/stereo_head_front_right/camera_info";
157      // CameraInfo subscriptions must use reliable + transient_local
158      // QoS in order to receive latched/history messages (even if only one
159      // message was published). Here we use keep_last(1) + reliable
160      // + transient_local.
161      sub_camerainfo_ = create_subscription<sensor_msgs::msg::CameraInfo>(
162          topic_name_,
163          rclcpp::QoS(rclcpp::KeepLast(1)).reliable().transient_local(),
164          std::bind(&StereoCameraTopicEcho::cb_camerainfo, this,
165                    std::placeholders::_1));
166      RCLCPP_INFO(get_logger(),
167                  "✅ Subscribing Right CameraInfo (with transient_local): %s",
168                  topic_name_.c_str());
169
170      // Unknown topic_type error
171    } else {
172      RCLCPP_ERROR(get_logger(), "Unknown topic_type: %s", topic_type_.c_str());
173      throw std::runtime_error("Unknown topic_type");
174    }
175  }
176
177  ~StereoCameraTopicEcho() override {
178    if (video_writer_.isOpened()) {
179      video_writer_.release();
180      RCLCPP_INFO(get_logger(), "Video file closed.");
181    }
182  }
183
184private:
185  // Image callback (left/right RGB image)
186  void cb_image(const sensor_msgs::msg::Image::SharedPtr msg) {
187    update_arrivals();
188
189    if (should_print()) {
190      RCLCPP_INFO(get_logger(),
191                  "📸 %s received\n"
192                  "  • frame_id:        %s\n"
193                  "  • stamp (sec):     %.6f\n"
194                  "  • encoding:        %s\n"
195                  "  • size (WxH):      %u x %u\n"
196                  "  • step (bytes/row):%u\n"
197                  "  • is_bigendian:    %u\n"
198                  "  • recv FPS (1s):   %.1f",
199                  topic_type_.c_str(), msg->header.frame_id.c_str(),
200                  rclcpp::Time(msg->header.stamp).seconds(),
201                  msg->encoding.c_str(), msg->width, msg->height, msg->step,
202                  msg->is_bigendian, get_fps());
203    }
204
205    // Video dump is supported only for RGB images
206    if ((topic_type_ == "left_rgb_image" || topic_type_ == "right_rgb_image") &&
207        !dump_video_path_.empty()) {
208      dump_image_to_video(msg);
209    }
210  }
211
212  // CompressedImage callback (left/right RGB compressed image)
213  void cb_compressed(const sensor_msgs::msg::CompressedImage::SharedPtr msg) {
214    update_arrivals();
215
216    if (should_print()) {
217      RCLCPP_INFO(get_logger(),
218                  "🗜️  %s received\n"
219                  "  • frame_id:        %s\n"
220                  "  • stamp (sec):     %.6f\n"
221                  "  • format:          %s\n"
222                  "  • data size:       %zu\n"
223                  "  • recv FPS (1s):   %.1f",
224                  topic_type_.c_str(), msg->header.frame_id.c_str(),
225                  rclcpp::Time(msg->header.stamp).seconds(),
226                  msg->format.c_str(), msg->data.size(), get_fps());
227    }
228  }
229
230  // CameraInfo callback (left/right camera intrinsic parameters)
231  void cb_camerainfo(const sensor_msgs::msg::CameraInfo::SharedPtr msg) {
232    // CameraInfo is typically published once; print it once
233    std::ostringstream oss;
234    oss << "📷 " << topic_type_ << " received\n"
235        << "  • frame_id:        " << msg->header.frame_id << "\n"
236        << "  • stamp (sec):     " << rclcpp::Time(msg->header.stamp).seconds()
237        << "\n"
238        << "  • width x height:  " << msg->width << " x " << msg->height << "\n"
239        << "  • distortion_model:" << msg->distortion_model << "\n"
240        << "  • D: [";
241    for (size_t i = 0; i < msg->d.size(); ++i) {
242      oss << msg->d[i];
243      if (i + 1 < msg->d.size())
244        oss << ", ";
245    }
246    oss << "]\n  • K: [";
247    for (int i = 0; i < 9; ++i) {
248      oss << msg->k[i];
249      if (i + 1 < 9)
250        oss << ", ";
251    }
252    oss << "]\n  • R: [";
253    for (int i = 0; i < 9; ++i) {
254      oss << msg->r[i];
255      if (i + 1 < 9)
256        oss << ", ";
257    }
258    oss << "]\n  • P: [";
259    for (int i = 0; i < 12; ++i) {
260      oss << msg->p[i];
261      if (i + 1 < 12)
262        oss << ", ";
263    }
264    oss << "]\n"
265        << "  • binning_x: " << msg->binning_x << "\n"
266        << "  • binning_y: " << msg->binning_y << "\n"
267        << "  • roi: { x_offset: " << msg->roi.x_offset
268        << ", y_offset: " << msg->roi.y_offset
269        << ", height: " << msg->roi.height << ", width: " << msg->roi.width
270        << ", do_rectify: " << (msg->roi.do_rectify ? "true" : "false") << " }";
271    RCLCPP_INFO(get_logger(), "%s", oss.str().c_str());
272  }
273
274  // Track arrival timestamps to compute FPS
275  void update_arrivals() {
276    const rclcpp::Time now = this->get_clock()->now();
277    arrivals_.push_back(now);
278    while (!arrivals_.empty() && (now - arrivals_.front()).seconds() > 1.0) {
279      arrivals_.pop_front();
280    }
281  }
282  double get_fps() const { return static_cast<double>(arrivals_.size()); }
283
284  // Control printing frequency
285  bool should_print() {
286    const rclcpp::Time now = this->get_clock()->now();
287    if ((now - last_print_).seconds() >= 1.0) {
288      last_print_ = now;
289      return true;
290    }
291    return false;
292  }
293
294  // Dump received images to a video file (RGB images only)
295  void dump_image_to_video(const sensor_msgs::msg::Image::SharedPtr &msg) {
296    cv::Mat image;
297    try {
298      // Obtain the Mat without copying by not specifying encoding
299      cv_bridge::CvImageConstPtr cvp = cv_bridge::toCvShare(msg);
300      image = cvp->image;
301      // Convert to BGR for uniform saving
302      if (msg->encoding == "rgb8") {
303        cv::cvtColor(image, image, cv::COLOR_RGB2BGR);
304      } else {
305        RCLCPP_WARN(get_logger(), "image encoding not expected: %s",
306                    msg->encoding.c_str());
307        return;
308      }
309    } catch (const std::exception &e) {
310      RCLCPP_WARN(get_logger(), "cv_bridge exception: %s", e.what());
311      return;
312    }
313
314    // Initialize VideoWriter
315    if (!video_writer_.isOpened()) {
316      int fourcc = cv::VideoWriter::fourcc('M', 'J', 'P', 'G');
317      double fps = std::max(1.0, get_fps());
318      bool ok = video_writer_.open(dump_video_path_, fourcc, fps,
319                                   cv::Size(image.cols, image.rows), true);
320      if (!ok) {
321        RCLCPP_ERROR(get_logger(), "Failed to open video file: %s",
322                     dump_video_path_.c_str());
323        dump_video_path_.clear(); // stop trying
324        return;
325      }
326      RCLCPP_INFO(get_logger(), "VideoWriter started: %s, size=%dx%d, fps=%.1f",
327                  dump_video_path_.c_str(), image.cols, image.rows, fps);
328    }
329    video_writer_.write(image);
330  }
331
332  // Member variables
333  std::string topic_type_;
334  std::string topic_name_;
335  std::string dump_video_path_;
336
337  // Subscriptions
338  rclcpp::Subscription<sensor_msgs::msg::Image>::SharedPtr sub_image_;
339  rclcpp::Subscription<sensor_msgs::msg::CompressedImage>::SharedPtr
340      sub_compressed_;
341  rclcpp::Subscription<sensor_msgs::msg::CameraInfo>::SharedPtr sub_camerainfo_;
342
343  // FPS statistics
344  rclcpp::Time last_print_{0, 0, RCL_ROS_TIME};
345  std::deque<rclcpp::Time> arrivals_;
346
347  // Video writer
348  cv::VideoWriter video_writer_;
349};
350
351int main(int argc, char **argv) {
352  rclcpp::init(argc, argv);
353  auto node = std::make_shared<StereoCameraTopicEcho>();
354  rclcpp::spin(node);
355  rclcpp::shutdown();
356  return 0;
357}

使用说明:

  1. 订阅左相机RGB图像:

    ros2 run examples echo_camera_stereo --ros-args -p topic_type:=left_rgb_image
    
  2. 订阅右相机RGB图像:

    ros2 run examples echo_camera_stereo --ros-args -p topic_type:=right_rgb_image
    
  3. 订阅左相机内参:

    ros2 run examples echo_camera_stereo --ros-args -p topic_type:=left_camera_info
    
  4. 录制左相机视频:

    # dump_video_path的值可改为其他路径, 注意提前创建该文件所在目录才能保存
    ros2 run examples echo_camera_stereo --ros-args -p topic_type:=left_rgb_image -p dump_video_path:=$PWD/left_camera.avi
    

头部后置单目相机数据订阅

该示例中用到了echo_camera_head_rear,通过订阅/aima/hal/sensor/rgb_head_rear/话题来接收机器人的头部后置单目相机数据,支持RGB图(及其mask处理)、压缩图和相机内参数据。

功能特点:

  • 支持头部后置相机数据订阅

  • 实时FPS统计和数据显示

  • 支持RGB图视频录制功能及把手遮挡区域mask处理

  • 可配置的topic类型选择

支持的数据类型:

  • rgb_image: RGB图像 (sensor_msgs/Image)

  • rgb_image_compressed: 压缩RGB图像 (sensor_msgs/CompressedImage)

  • camera_info: 相机内参 (sensor_msgs/CameraInfo)

  1#include <deque>
  2#include <filesystem>
  3#include <iomanip>
  4#include <memory>
  5#include <rclcpp/rclcpp.hpp>
  6#include <sensor_msgs/msg/camera_info.hpp>
  7#include <sensor_msgs/msg/compressed_image.hpp>
  8#include <sensor_msgs/msg/image.hpp>
  9#include <sstream>
 10#include <string>
 11#include <vector>
 12
 13// OpenCV headers for image/video writing
 14#include <cv_bridge/cv_bridge.h>
 15#include <opencv2/opencv.hpp>
 16
 17/**
 18 * @brief Example of subscribing to multiple topics for the rear head monocular
 19 * camera
 20 *
 21 * You can select which topic type to subscribe to via the startup argument
 22 * --ros-args -p topic_type:=<type>:
 23 *   - rgb_image: RGB image (sensor_msgs/Image)
 24 *   - rgb_image_compressed: RGB compressed image (sensor_msgs/CompressedImage)
 25 *   - camera_info: Camera intrinsic parameters (sensor_msgs/CameraInfo)
 26 *
 27 * Examples:
 28 *   ros2 run examples echo_camera_head_rear --ros-args -p topic_type:=rgb_image
 29 *   ros2 run examples echo_camera_head_rear --ros-args -p
 30 * topic_type:=rgb_image_compressed ros2 run examples echo_camera_head_rear
 31 * --ros-args -p topic_type:=camera_info
 32 *
 33 * topic_type defaults to "rgb_image"
 34 *
 35 * See individual callbacks for more detailed comments
 36 */
 37class HeadRearCameraTopicEcho : public rclcpp::Node {
 38public:
 39  HeadRearCameraTopicEcho() : Node("head_rear_camera_topic_echo") {
 40    // Select which topic type to subscribe to
 41    topic_type_ = declare_parameter<std::string>("topic_type", "rgb_image");
 42    dump_video_path_ = declare_parameter<std::string>("dump_video_path", "");
 43    with_mask_ = declare_parameter<bool>("with_mask", false);
 44
 45    // Subscribed topics and their message layouts
 46    // 1. /aima/hal/sensor/rgb_head_rear/rgb_image
 47    //    - topic_type: rgb_image
 48    //    - message type: sensor_msgs::msg::Image
 49    //    - frame_id: rgb_head_rear
 50    //    - child_frame_id: /
 51    //    - contents: raw image data
 52    // 2. /aima/hal/sensor/rgb_head_rear/rgb_image/compressed
 53    //    - topic_type: rgb_image_compressed
 54    //    - message type: sensor_msgs::msg::CompressedImage
 55    //    - frame_id: rgb_head_rear
 56    //    - contents: compressed image data
 57    // 3. /aima/hal/sensor/rgb_head_rear/camera_info
 58    //    - topic_type: camera_info
 59    //    - message type: sensor_msgs::msg::CameraInfo
 60    //    - frame_id: rgb_head_rear
 61    //    - contents: camera intrinsic parameters
 62
 63    // Set QoS parameters - use SensorData QoS
 64    auto qos = rclcpp::SensorDataQoS();
 65
 66    if (with_mask_ && !dump_video_path_.empty()) {
 67      auto mask_path =
 68          std::filesystem::read_symlink("/proc/self/exe").parent_path() /
 69          "data" / "rgb_head_rear_mask.png";
 70      mask_image_ = cv::imread(mask_path, cv::IMREAD_GRAYSCALE);
 71      if (mask_image_.empty()) {
 72        RCLCPP_ERROR(get_logger(), "Failed to load mask file from %s",
 73                     mask_path.c_str());
 74        throw std::runtime_error("Failed to load mask file");
 75      }
 76    }
 77
 78    // Enable RGB image subscription
 79    if (topic_type_ == "rgb_image") {
 80      topic_name_ = "/aima/hal/sensor/rgb_head_rear/rgb_image";
 81      sub_image_ = create_subscription<sensor_msgs::msg::Image>(
 82          topic_name_, qos,
 83          std::bind(&HeadRearCameraTopicEcho::cb_image, this,
 84                    std::placeholders::_1));
 85      RCLCPP_INFO(get_logger(), "✅ Subscribing RGB Image: %s",
 86                  topic_name_.c_str());
 87      if (!dump_video_path_.empty()) {
 88        RCLCPP_INFO(
 89            get_logger(), "📝 Will dump received images %s mask to video: %s",
 90            (with_mask_ ? "with" : "without"), dump_video_path_.c_str());
 91      }
 92    }
 93
 94    // Enable RGB compressed image subscription
 95    else if (topic_type_ == "rgb_image_compressed") {
 96      topic_name_ = "/aima/hal/sensor/rgb_head_rear/rgb_image/compressed";
 97      sub_compressed_ = create_subscription<sensor_msgs::msg::CompressedImage>(
 98          topic_name_, qos,
 99          std::bind(&HeadRearCameraTopicEcho::cb_compressed, this,
100                    std::placeholders::_1));
101      RCLCPP_INFO(get_logger(), "✅ Subscribing CompressedImage: %s",
102                  topic_name_.c_str());
103
104      // Enable camera info subscription
105    } else if (topic_type_ == "camera_info") {
106      topic_name_ = "/aima/hal/sensor/rgb_head_rear/camera_info";
107      // CameraInfo subscriptions must use reliable + transient_local
108      // QoS in order to receive latched/history messages (even if only one
109      // message was published). Here we use keep_last(1) + reliable
110      // + transient_local.
111      sub_camerainfo_ = create_subscription<sensor_msgs::msg::CameraInfo>(
112          topic_name_,
113          rclcpp::QoS(rclcpp::KeepLast(1)).reliable().transient_local(),
114          std::bind(&HeadRearCameraTopicEcho::cb_camerainfo, this,
115                    std::placeholders::_1));
116      RCLCPP_INFO(get_logger(),
117                  "✅ Subscribing CameraInfo (with transient_local): %s",
118                  topic_name_.c_str());
119
120      // Unknown topic_type error
121    } else {
122      RCLCPP_ERROR(get_logger(), "Unknown topic_type: %s", topic_type_.c_str());
123      throw std::runtime_error("Unknown topic_type");
124    }
125  }
126
127  ~HeadRearCameraTopicEcho() override {
128    if (video_writer_.isOpened()) {
129      video_writer_.release();
130      RCLCPP_INFO(get_logger(), "Video file closed.");
131    }
132  }
133
134private:
135  // Image callback (RGB image)
136  void cb_image(const sensor_msgs::msg::Image::SharedPtr msg) {
137    update_arrivals();
138
139    if (should_print()) {
140      RCLCPP_INFO(get_logger(),
141                  "📸 %s received\n"
142                  "  • frame_id:        %s\n"
143                  "  • stamp (sec):     %.6f\n"
144                  "  • encoding:        %s\n"
145                  "  • size (WxH):      %u x %u\n"
146                  "  • step (bytes/row):%u\n"
147                  "  • is_bigendian:    %u\n"
148                  "  • recv FPS (1s):   %.1f",
149                  topic_type_.c_str(), msg->header.frame_id.c_str(),
150                  rclcpp::Time(msg->header.stamp).seconds(),
151                  msg->encoding.c_str(), msg->width, msg->height, msg->step,
152                  msg->is_bigendian, get_fps());
153    }
154
155    // Video dump is supported only for RGB images
156    if (topic_type_ == "rgb_image" && !dump_video_path_.empty()) {
157      dump_image_to_video(msg);
158    }
159  }
160
161  // CompressedImage callback (RGB compressed image)
162  void cb_compressed(const sensor_msgs::msg::CompressedImage::SharedPtr msg) {
163    update_arrivals();
164
165    if (should_print()) {
166      RCLCPP_INFO(get_logger(),
167                  "🗜️  %s received\n"
168                  "  • frame_id:        %s\n"
169                  "  • stamp (sec):     %.6f\n"
170                  "  • format:          %s\n"
171                  "  • data size:       %zu\n"
172                  "  • recv FPS (1s):   %.1f",
173                  topic_type_.c_str(), msg->header.frame_id.c_str(),
174                  rclcpp::Time(msg->header.stamp).seconds(),
175                  msg->format.c_str(), msg->data.size(), get_fps());
176    }
177  }
178
179  // CameraInfo callback (camera intrinsic parameters)
180  void cb_camerainfo(const sensor_msgs::msg::CameraInfo::SharedPtr msg) {
181    // CameraInfo is typically published once; print it once
182    std::ostringstream oss;
183    oss << "📷 " << topic_type_ << " received\n"
184        << "  • frame_id:        " << msg->header.frame_id << "\n"
185        << "  • stamp (sec):     " << rclcpp::Time(msg->header.stamp).seconds()
186        << "\n"
187        << "  • width x height:  " << msg->width << " x " << msg->height << "\n"
188        << "  • distortion_model:" << msg->distortion_model << "\n"
189        << "  • D: [";
190    for (size_t i = 0; i < msg->d.size(); ++i) {
191      oss << msg->d[i];
192      if (i + 1 < msg->d.size())
193        oss << ", ";
194    }
195    oss << "]\n  • K: [";
196    for (int i = 0; i < 9; ++i) {
197      oss << msg->k[i];
198      if (i + 1 < 9)
199        oss << ", ";
200    }
201    oss << "]\n  • R: [";
202    for (int i = 0; i < 9; ++i) {
203      oss << msg->r[i];
204      if (i + 1 < 9)
205        oss << ", ";
206    }
207    oss << "]\n  • P: [";
208    for (int i = 0; i < 12; ++i) {
209      oss << msg->p[i];
210      if (i + 1 < 12)
211        oss << ", ";
212    }
213    oss << "]\n"
214        << "  • binning_x: " << msg->binning_x << "\n"
215        << "  • binning_y: " << msg->binning_y << "\n"
216        << "  • roi: { x_offset: " << msg->roi.x_offset
217        << ", y_offset: " << msg->roi.y_offset
218        << ", height: " << msg->roi.height << ", width: " << msg->roi.width
219        << ", do_rectify: " << (msg->roi.do_rectify ? "true" : "false") << " }";
220    RCLCPP_INFO(get_logger(), "%s", oss.str().c_str());
221  }
222
223  // Track arrival timestamps to compute FPS
224  void update_arrivals() {
225    const rclcpp::Time now = this->get_clock()->now();
226    arrivals_.push_back(now);
227    while (!arrivals_.empty() && (now - arrivals_.front()).seconds() > 1.0) {
228      arrivals_.pop_front();
229    }
230  }
231  double get_fps() const { return static_cast<double>(arrivals_.size()); }
232
233  // Control printing frequency
234  bool should_print() {
235    const rclcpp::Time now = this->get_clock()->now();
236    if ((now - last_print_).seconds() >= 1.0) {
237      last_print_ = now;
238      return true;
239    }
240    return false;
241  }
242
243  // Dump received images to a video file (RGB images only)
244  void dump_image_to_video(const sensor_msgs::msg::Image::SharedPtr &msg) {
245    cv::Mat image;
246    try {
247      // Obtain the Mat without copying by not specifying encoding
248      cv_bridge::CvImageConstPtr cvp = cv_bridge::toCvShare(msg);
249      image = cvp->image;
250      // Convert to BGR for uniform saving
251      if (msg->encoding == "rgb8") {
252        cv::cvtColor(image, image, cv::COLOR_RGB2BGR);
253      } else {
254        RCLCPP_WARN(get_logger(), "image encoding not expected: %s",
255                    msg->encoding.c_str());
256        return;
257      }
258      if (with_mask_) {
259        image.setTo(cv::Scalar(0, 0, 0), mask_image_ == 0);
260      }
261    } catch (const std::exception &e) {
262      RCLCPP_WARN(get_logger(), "cv_bridge exception: %s", e.what());
263      return;
264    }
265
266    // Initialize VideoWriter
267    if (!video_writer_.isOpened()) {
268      int fourcc = cv::VideoWriter::fourcc('M', 'J', 'P', 'G');
269      double fps = std::max(1.0, get_fps());
270      bool ok = video_writer_.open(dump_video_path_, fourcc, fps,
271                                   cv::Size(image.cols, image.rows), true);
272      if (!ok) {
273        RCLCPP_ERROR(get_logger(), "Failed to open video file: %s",
274                     dump_video_path_.c_str());
275        dump_video_path_.clear(); // stop trying
276        return;
277      }
278      RCLCPP_INFO(get_logger(), "VideoWriter started: %s, size=%dx%d, fps=%.1f",
279                  dump_video_path_.c_str(), image.cols, image.rows, fps);
280    }
281    video_writer_.write(image);
282  }
283
284  // Member variables
285  std::string topic_type_;
286  std::string topic_name_;
287  std::string dump_video_path_;
288  bool with_mask_;
289  cv::Mat mask_image_;
290
291  // Subscriptions
292  rclcpp::Subscription<sensor_msgs::msg::Image>::SharedPtr sub_image_;
293  rclcpp::Subscription<sensor_msgs::msg::CompressedImage>::SharedPtr
294      sub_compressed_;
295  rclcpp::Subscription<sensor_msgs::msg::CameraInfo>::SharedPtr sub_camerainfo_;
296
297  // FPS statistics
298  rclcpp::Time last_print_{0, 0, RCL_ROS_TIME};
299  std::deque<rclcpp::Time> arrivals_;
300
301  // Video writer
302  cv::VideoWriter video_writer_;
303};
304
305int main(int argc, char **argv) {
306  rclcpp::init(argc, argv);
307  auto node = std::make_shared<HeadRearCameraTopicEcho>();
308  rclcpp::spin(node);
309  rclcpp::shutdown();
310  return 0;
311}

使用说明:

  1. 订阅RGB图像数据:

    ros2 run examples echo_camera_head_rear --ros-args -p topic_type:=rgb_image
    
  2. 订阅压缩图像数据:

    ros2 run examples echo_camera_head_rear --ros-args -p topic_type:=rgb_image_compressed
    
  3. 订阅相机内参:

    ros2 run examples echo_camera_head_rear --ros-args -p topic_type:=camera_info
    
  4. 录制视频:

    # dump_video_path的值可改为其他路径, 注意提前创建该文件所在目录才能保存
    ros2 run examples echo_camera_head_rear --ros-args -p topic_type:=rgb_image -p dump_video_path:=$PWD/rear_camera.avi
    
  5. 录制视频并对把手遮挡区域mask处理:

    # dump_video_path的值可改为其他路径, 注意提前创建该文件所在目录才能保存
    ros2 run examples echo_camera_head_rear --ros-args -p topic_type:=rgb_image -p with_mask:=true -p dump_video_path:=$PWD/rear_camera.avi
    

应用场景:

  • 人脸识别和追踪

  • 目标检测和识别

  • 视觉SLAM

  • 图像处理和计算机视觉算法开发

  • 机器人视觉导航

6.2.13 头部触摸传感器数据订阅

该示例中用到了 echo_head_touch_sensor,通过订阅/aima/hal/sensor/touch_head话题来接收机器人的头部触摸传感器的反馈数据。

功能特点:

  • 订阅了头部传感器的反馈数据,当头部被触摸时候,输出会从 IDLE->TOUCH

 1//
 2// Created by agiuser on 2026/1/23.
 3//
 4
 5#include <aimdk_msgs/msg/touch_state.hpp>
 6#include <rclcpp/rclcpp.hpp>
 7
 8class TouchStateSubscriber : public rclcpp::Node {
 9public:
10  TouchStateSubscriber() : Node("touch_state_subscriber") {
11    subscription_ = this->create_subscription<aimdk_msgs::msg::TouchState>(
12        "/aima/hal/sensor/touch_head", 10,
13        std::bind(&TouchStateSubscriber::touch_callback, this,
14                  std::placeholders::_1));
15
16    RCLCPP_INFO(this->get_logger(), "TouchState subscriber started, listening "
17                                    "to /aima/hal/sensor/touch_head");
18  }
19
20private:
21  void touch_callback(const aimdk_msgs::msg::TouchState::SharedPtr msg) {
22    // print message info
23    RCLCPP_INFO(this->get_logger(), "Received TouchState message:");
24    RCLCPP_INFO(this->get_logger(), "  Timestamp: %d.%09d",
25                msg->header.stamp.sec, msg->header.stamp.nanosec);
26
27    std::string event_str = get_event_type_string(msg->event_type);
28    RCLCPP_INFO(this->get_logger(), "  Event Type: %s (%d)", event_str.c_str(),
29                msg->event_type);
30  }
31
32  std::string get_event_type_string(uint8_t event_type) {
33    switch (event_type) {
34    case aimdk_msgs::msg::TouchState::UNKNOWN:
35      return "UNKNOWN";
36    case aimdk_msgs::msg::TouchState::IDLE:
37      return "IDLE";
38    case aimdk_msgs::msg::TouchState::TOUCH:
39      return "TOUCH";
40    case aimdk_msgs::msg::TouchState::SLIDE:
41      return "SLIDE";
42    case aimdk_msgs::msg::TouchState::PAT_ONCE:
43      return "PAT_ONCE";
44    case aimdk_msgs::msg::TouchState::PAT_TWICE:
45      return "PAT_TWICE";
46    case aimdk_msgs::msg::TouchState::PAT_TRIPLE:
47      return "PAT_TRIPLE";
48    default:
49      return "INVALID";
50    }
51  }
52  rclcpp::Subscription<aimdk_msgs::msg::TouchState>::SharedPtr subscription_;
53};
54
55int main(int argc, char *argv[]) {
56  rclcpp::init(argc, argv);
57  auto node = std::make_shared<TouchStateSubscriber>();
58  rclcpp::spin(node);
59  rclcpp::shutdown();
60  return 0;
61}

使用说明:

ros2 ros2 run examples echo_head_touch_sensor

输出示例:

[INFO] [1769162721.359354722] [touch_state_subscriber]:   Timestamp: 1769162726.863282315
[INFO] [1769162721.359361643] [touch_state_subscriber]:   Event Type: IDLE (1)
[INFO] [1769167184.142143492] [touch_state_subscriber]:   Timestamp: 1769167189.364879133
[INFO] [1769167184.142147126] [touch_state_subscriber]:   Event Type: TOUCH (2)

6.2.14 激光雷达数据订阅

该示例中用到了echo_lidar_data,通过订阅/aima/hal/sensor/lidar_chest_front/话题来接收机器人的激光雷达数据,支持点云数据和IMU数据两种数据类型。

功能特点:

  • 支持激光雷达点云数据订阅

  • 支持激光雷达IMU数据订阅

  • 实时FPS统计和数据显示

  • 可配置的topic类型选择

  • 详细的数据字段信息输出

支持的数据类型:

  • PointCloud2: 激光雷达点云数据 (sensor_msgs/PointCloud2)

  • Imu: 激光雷达IMU数据 (sensor_msgs/Imu)

技术实现:

  • 使用SensorDataQoS配置(BEST_EFFORT + VOLATILE

  • 支持点云字段信息解析和显示

  • 支持IMU四元数、角速度和线性加速度数据

  • 提供详细的调试日志输出

应用场景:

  • 激光雷达数据采集和分析

  • 点云数据处理和可视化

  • 机器人导航和定位

  • SLAM算法开发

  • 环境感知和建图

  1#include <deque>
  2#include <iomanip>
  3#include <memory>
  4#include <rclcpp/rclcpp.hpp>
  5#include <sensor_msgs/msg/imu.hpp>
  6#include <sensor_msgs/msg/point_cloud2.hpp>
  7#include <sstream>
  8#include <string>
  9#include <vector>
 10
 11/**
 12 * @brief Example for subscribing to chest LIDAR data
 13 *
 14 * Supports subscribing to the following topics:
 15 *   1. /aima/hal/sensor/lidar_chest_front/lidar_pointcloud
 16 *      - Data type: sensor_msgs::msg::PointCloud2
 17 *      - frame_id: lidar_chest_front
 18 *      - child_frame_id: /
 19 *      - Content: LIDAR point cloud data
 20 *   2. /aima/hal/sensor/lidar_chest_front/imu
 21 *      - Data type: sensor_msgs::msg::Imu
 22 *      - frame_id: lidar_imu_chest_front
 23 *      - Content: LIDAR IMU data
 24 *
 25 * You can select the topic type to subscribe to using the launch parameter
 26 * --ros-args -p topic_type:=<type>:
 27 *   - pointcloud: Subscribe to LIDAR point cloud
 28 *   - imu: Subscribe to LIDAR IMU
 29 * The default topic_type is pointcloud
 30 */
 31class LidarChestEcho : public rclcpp::Node {
 32public:
 33  LidarChestEcho() : Node("lidar_chest_echo") {
 34    topic_type_ = declare_parameter<std::string>("topic_type", "pointcloud");
 35
 36    auto qos = rclcpp::SensorDataQoS();
 37
 38    if (topic_type_ == "pointcloud") {
 39      topic_name_ = "/aima/hal/sensor/lidar_chest_front/lidar_pointcloud";
 40      sub_pointcloud_ = create_subscription<sensor_msgs::msg::PointCloud2>(
 41          topic_name_, qos,
 42          std::bind(&LidarChestEcho::cb_pointcloud, this,
 43                    std::placeholders::_1));
 44      RCLCPP_INFO(get_logger(), "✅ Subscribing LIDAR PointCloud2: %s",
 45                  topic_name_.c_str());
 46    } else if (topic_type_ == "imu") {
 47      topic_name_ = "/aima/hal/sensor/lidar_chest_front/imu";
 48      sub_imu_ = create_subscription<sensor_msgs::msg::Imu>(
 49          topic_name_, qos,
 50          std::bind(&LidarChestEcho::cb_imu, this, std::placeholders::_1));
 51      RCLCPP_INFO(get_logger(), "✅ Subscribing LIDAR IMU: %s",
 52                  topic_name_.c_str());
 53    } else {
 54      RCLCPP_ERROR(get_logger(), "Unknown topic_type: %s", topic_type_.c_str());
 55      throw std::runtime_error("Unknown topic_type");
 56    }
 57  }
 58
 59private:
 60  // PointCloud2 callback
 61  void cb_pointcloud(const sensor_msgs::msg::PointCloud2::SharedPtr msg) {
 62    update_arrivals();
 63
 64    if (should_print()) {
 65      std::ostringstream oss;
 66      oss << "🟢 LIDAR PointCloud2 received\n"
 67          << "  • frame_id:        " << msg->header.frame_id << "\n"
 68          << "  • stamp (sec):     "
 69          << rclcpp::Time(msg->header.stamp).seconds() << "\n"
 70          << "  • width x height:  " << msg->width << " x " << msg->height
 71          << "\n"
 72          << "  • point_step:      " << msg->point_step << "\n"
 73          << "  • row_step:        " << msg->row_step << "\n"
 74          << "  • fields:          ";
 75      for (const auto &f : msg->fields)
 76        oss << f.name << "(" << (int)f.datatype << ") ";
 77      oss << "\n  • is_bigendian:    " << msg->is_bigendian
 78          << "\n  • is_dense:        " << msg->is_dense
 79          << "\n  • data size:       " << msg->data.size()
 80          << "\n  • recv FPS (1s):   " << get_fps();
 81      RCLCPP_INFO(get_logger(), "%s", oss.str().c_str());
 82    }
 83  }
 84
 85  // IMU callback
 86  void cb_imu(const sensor_msgs::msg::Imu::SharedPtr msg) {
 87    update_arrivals();
 88
 89    if (should_print()) {
 90      std::ostringstream oss;
 91      oss << "🟢 LIDAR IMU received\n"
 92          << "  • frame_id:        " << msg->header.frame_id << "\n"
 93          << "  • stamp (sec):     "
 94          << rclcpp::Time(msg->header.stamp).seconds() << "\n"
 95          << "  • orientation:     [" << msg->orientation.x << ", "
 96          << msg->orientation.y << ", " << msg->orientation.z << ", "
 97          << msg->orientation.w << "]\n"
 98          << "  • angular_velocity:[" << msg->angular_velocity.x << ", "
 99          << msg->angular_velocity.y << ", " << msg->angular_velocity.z << "]\n"
100          << "  • linear_accel:    [" << msg->linear_acceleration.x << ", "
101          << msg->linear_acceleration.y << ", " << msg->linear_acceleration.z
102          << "]\n"
103          << "  • recv FPS (1s):   " << get_fps();
104      RCLCPP_INFO(get_logger(), "%s", oss.str().c_str());
105    }
106  }
107
108  // Update FPS statistics
109  void update_arrivals() {
110    const rclcpp::Time now = this->get_clock()->now();
111    arrivals_.push_back(now);
112    while (!arrivals_.empty() && (now - arrivals_.front()).seconds() > 1.0) {
113      arrivals_.pop_front();
114    }
115  }
116  double get_fps() const { return static_cast<double>(arrivals_.size()); }
117
118  // Control print frequency
119  bool should_print() {
120    const rclcpp::Time now = this->get_clock()->now();
121    if ((now - last_print_).seconds() >= 1.0) {
122      last_print_ = now;
123      return true;
124    }
125    return false;
126  }
127
128  // Member variables
129  std::string topic_type_;
130  std::string topic_name_;
131
132  rclcpp::Subscription<sensor_msgs::msg::PointCloud2>::SharedPtr
133      sub_pointcloud_;
134  rclcpp::Subscription<sensor_msgs::msg::Imu>::SharedPtr sub_imu_;
135
136  rclcpp::Time last_print_{0, 0, RCL_ROS_TIME};
137  std::deque<rclcpp::Time> arrivals_;
138};
139
140int main(int argc, char **argv) {
141  rclcpp::init(argc, argv);
142  auto node = std::make_shared<LidarChestEcho>();
143  rclcpp::spin(node);
144  rclcpp::shutdown();
145  return 0;
146}

使用说明:

# 订阅激光雷达点云数据
ros2 run examples echo_lidar_data --ros-args -p topic_type:=pointcloud

# 订阅激光雷达IMU数据
ros2 run examples echo_lidar_data --ros-args -p topic_type:=imu

输出示例:

[INFO] [lidar_chest_echo]: ✅ Subscribing LIDAR PointCloud2: /aima/hal/sensor/lidar_chest_front/lidar_pointcloud
[INFO] [lidar_chest_echo]: 🟢 LIDAR PointCloud2 received
  • frame_id:        lidar_chest_front
  • stamp (sec):     1234567890.123456
  • width x height:  1 x 36000
  • point_step:      16
  • row_step:        16
  • fields:          x(7) y(7) z(7) intensity(7)
  • is_bigendian:    False
  • is_dense:        True
  • data size:       576000
  • recv FPS (1s):   10.0

6.2.15 播放视频

该示例中用到了play_video,在运行节点程序前,需要先将视频上传到机器人的**交互计算单元(PC3)**上(用户可在其上创建一个用来存储视频的目录如/var/tmp/videos/),然后将节点程序中的video_path改为需要播放视频的路径。

注意

⚠️ 请注意!交互计算单元(PC3)独立于二开程序所在的开发计算单元(PC2), 音视频文件务必存入交互计算单元(IP: 10.0.1.42)。
⚠️ 音视频文件夹及该文件夹所有父目录应当为所有用户可访问读取(建议在/var/tmp/下创建子目录存放)

功能说明
通过调用PlayVideo服务,可以让机器人在屏幕上播放指定路径的视频文件。请确保视频文件已上传到交互计算单元,否则播放会失败。

  1#include "aimdk_msgs/srv/play_video.hpp"
  2#include "aimdk_msgs/msg/common_request.hpp"
  3#include "rclcpp/rclcpp.hpp"
  4#include <chrono>
  5#include <memory>
  6#include <signal.h>
  7#include <string>
  8
  9// Global variable used for signal handling
 10std::shared_ptr<rclcpp::Node> g_node = nullptr;
 11
 12// Signal handler function
 13void signal_handler(int signal) {
 14  if (g_node) {
 15    RCLCPP_INFO(g_node->get_logger(), "Received signal %d, shutting down...",
 16                signal);
 17    g_node.reset();
 18  }
 19  rclcpp::shutdown();
 20  exit(signal);
 21}
 22
 23class PlayVideoClient : public rclcpp::Node {
 24public:
 25  PlayVideoClient() : Node("play_video_client") {
 26    client_ = this->create_client<aimdk_msgs::srv::PlayVideo>(
 27        "/face_ui_proxy/play_video");
 28    RCLCPP_INFO(this->get_logger(), "✅ PlayVideo client node started.");
 29
 30    // Wait for the service to become available
 31    while (!client_->wait_for_service(std::chrono::seconds(2))) {
 32      RCLCPP_INFO(this->get_logger(), "⏳ Service unavailable, waiting...");
 33    }
 34    RCLCPP_INFO(this->get_logger(),
 35                "🟢 Service available, ready to send request.");
 36  }
 37
 38  bool send_request(const std::string &video_path, uint8_t mode,
 39                    int32_t priority) {
 40    try {
 41      auto request = std::make_shared<aimdk_msgs::srv::PlayVideo::Request>();
 42
 43      request->video_path = video_path;
 44      request->mode = mode;
 45      request->priority = priority;
 46
 47      RCLCPP_INFO(this->get_logger(),
 48                  "📨 Sending request to play video: mode=%hhu video=%s", mode,
 49                  video_path.c_str());
 50
 51      const std::chrono::milliseconds timeout(250);
 52      for (int i = 0; i < 8; i++) {
 53        request->header.header.stamp = this->now();
 54        auto future = client_->async_send_request(request);
 55        auto retcode = rclcpp::spin_until_future_complete(shared_from_this(),
 56                                                          future, timeout);
 57        if (retcode != rclcpp::FutureReturnCode::SUCCESS) {
 58          // retry as remote peer is NOT handled well by ROS
 59          RCLCPP_INFO(this->get_logger(), "trying ... [%d]", i);
 60          continue;
 61        }
 62        // future.done
 63        auto response = future.get();
 64        if (response->success) {
 65          RCLCPP_INFO(this->get_logger(),
 66                      "✅ Request to play video recorded successfully: %s",
 67                      response->message.c_str());
 68          return true;
 69        } else {
 70          RCLCPP_ERROR(this->get_logger(),
 71                       "❌ Failed to record play-video request: %s",
 72                       response->message.c_str());
 73          return false;
 74        }
 75      }
 76      RCLCPP_ERROR(this->get_logger(), "❌ Service call failed or timed out.");
 77      return false;
 78    } catch (const std::exception &e) {
 79      RCLCPP_ERROR(this->get_logger(), "Exception occurred: %s", e.what());
 80      return false;
 81    }
 82  }
 83
 84private:
 85  rclcpp::Client<aimdk_msgs::srv::PlayVideo>::SharedPtr client_;
 86};
 87
 88int main(int argc, char **argv) {
 89  try {
 90    rclcpp::init(argc, argv);
 91
 92    // Set up signal handlers
 93    signal(SIGINT, signal_handler);
 94    signal(SIGTERM, signal_handler);
 95
 96    std::string video_path =
 97        "/agibot/data/home/agi/zhiyuan.mp4"; // Default video path; modify as
 98                                             // needed
 99    int32_t priority = 5;
100    int mode = 2; // Loop playback
101    std::cout << "Enter video play mode (1: once, 2: loop): ";
102    std::cin >> mode;
103    if (mode < 1 || mode > 2) {
104      RCLCPP_ERROR(rclcpp::get_logger("main"), "Invalid play mode: %d", mode);
105      rclcpp::shutdown();
106      return 1;
107    }
108
109    g_node = std::make_shared<PlayVideoClient>();
110    auto client = std::dynamic_pointer_cast<PlayVideoClient>(g_node);
111
112    if (client) {
113      client->send_request(video_path, mode, priority);
114    }
115
116    // Clean up resources
117    g_node.reset();
118    rclcpp::shutdown();
119
120    return 0;
121  } catch (const std::exception &e) {
122    RCLCPP_ERROR(rclcpp::get_logger("main"),
123                 "Program exited with exception: %s", e.what());
124    return 1;
125  }
126}

6.2.16 媒体文件播放

该示例中用到了play_media,通过该节点可实现播放指定的媒体文件(如音频文件),支持WAV、MP3等格式的音频文件播放。

功能特点:

  • 支持多种音频格式播放(WAV、MP3等)

  • 支持优先级控制,可设置播放优先级

  • 支持打断机制,可中断当前播放

  • 支持自定义文件路径和播放参数

  • 提供完整的错误处理和状态反馈

技术实现:

  • 使用PlayMediaFile服务进行媒体文件播放

  • 支持优先级级别设置(0-99)

  • 支持中断控制(is_interrupted参数)

  • 提供详细的播放状态反馈

应用场景:

  • 音频文件播放和媒体控制

  • 语音提示和音效播放

  • 多媒体应用开发

  • 机器人交互音频反馈

注意

⚠️ 请注意!交互计算单元(PC3)独立于二开程序所在的开发计算单元(PC2), 音视频文件务必存入交互计算单元(IP: 10.0.1.42)。
⚠️ 音视频文件夹及该文件夹所有父目录应当为所有用户可访问读取(建议在/var/tmp/下创建子目录存放)

 1#include <aimdk_msgs/msg/tts_priority_level.hpp>
 2#include <aimdk_msgs/srv/play_media_file.hpp>
 3#include <iostream>
 4#include <rclcpp/rclcpp.hpp>
 5#include <string>
 6
 7using PlayMediaFile = aimdk_msgs::srv::PlayMediaFile;
 8
 9int main(int argc, char **argv) {
10  rclcpp::init(argc, argv);
11  auto node = rclcpp::Node::make_shared("play_media_file_client_min");
12
13  // 1) Service name
14  const std::string service_name = "/aimdk_5Fmsgs/srv/PlayMediaFile";
15  auto client = node->create_client<PlayMediaFile>(service_name);
16
17  // 2) Input file path (prompt user if not provided as argument)
18  std::string default_file =
19      "/agibot/data/var/interaction/tts_cache/normal/demo.wav";
20  std::string file_name;
21
22  if (argc > 1) {
23    file_name = argv[1];
24  } else {
25    std::cout << "Enter the media file path to play (default: " << default_file
26              << "): ";
27    std::getline(std::cin, file_name);
28    if (file_name.empty()) {
29      file_name = default_file;
30    }
31  }
32
33  // 3) Build the request
34  auto req = std::make_shared<PlayMediaFile::Request>();
35  // CommonRequest request -> RequestHeader header -> builtin_interfaces/Time
36  // stamp
37  req->header.header.stamp = node->now();
38
39  // PlayMediaFileRequest required fields
40  req->media_file_req.file_name = file_name;
41  req->media_file_req.domain = "demo_client"; // Required: identifies the caller
42  req->media_file_req.trace_id = "demo";      // Optional: request identifier
43  req->media_file_req.is_interrupted =
44      true; // Whether to interrupt same-priority playback
45  req->media_file_req.priority_weight = 0; // Optional: 0~99
46  // Priority level: default INTERACTION_L6
47  req->media_file_req.priority_level.value = 6;
48
49  // 4) Wait for service and call
50  RCLCPP_INFO(node->get_logger(), "Waiting for service: %s",
51              service_name.c_str());
52  if (!client->wait_for_service(std::chrono::seconds(5))) {
53    RCLCPP_ERROR(node->get_logger(), "Service unavailable: %s",
54                 service_name.c_str());
55    rclcpp::shutdown();
56    return 1;
57  }
58
59  auto future = client->async_send_request(req);
60  auto rc = rclcpp::spin_until_future_complete(node, future,
61                                               std::chrono::seconds(10));
62
63  if (rc == rclcpp::FutureReturnCode::INTERRUPTED) {
64    RCLCPP_WARN(node->get_logger(), "Interrupted while waiting");
65    rclcpp::shutdown();
66    return 1;
67  }
68
69  if (rc != rclcpp::FutureReturnCode::SUCCESS) {
70    RCLCPP_ERROR(node->get_logger(), "Call timed out or did not complete");
71    rclcpp::shutdown();
72    return 1;
73  }
74
75  // 5) Handle response (success is in tts_resp)
76  try {
77    const auto resp = future.get();
78    bool success = resp->tts_resp.is_success;
79
80    if (success) {
81      RCLCPP_INFO(node->get_logger(), "✅ Media file play request succeeded: %s",
82                  file_name.c_str());
83    } else {
84      RCLCPP_ERROR(node->get_logger(), "❌ Media file play request failed: %s",
85                   file_name.c_str());
86    }
87  } catch (const std::exception &e) {
88    RCLCPP_ERROR(node->get_logger(), "Call exception: %s", e.what());
89  }
90
91  rclcpp::shutdown();
92  return 0;
93}

使用说明:

# 播放默认音频文件
ros2 run examples play_media

# 播放指定音频文件
# 注意替换/path/to/your/audio_file.wav为交互板上实际文件路径
ros2 run examples play_media /path/to/your/audio_file.wav

# 播放TTS缓存文件
ros2 run examples play_media /agibot/data/var/interaction/tts_cache/normal/demo.wav

输出示例:

[INFO] [play_media_file_client_min]: ✅ 媒体文件播放请求成功

注意事项:

  • 确保音频文件路径正确且文件存在

  • 支持的文件格式:WAV、MP3等

  • 优先级设置影响播放队列顺序

  • 打断功能可中断当前播放的音频

6.2.17 TTS (文字转语音)

该示例中用到了play_tts,通过该节点可实现语音播放输入的文字,用户可根据不同的场景输入相应的文本。

功能特点:

  • 支持命令行参数和交互式输入

  • 完整的服务可用性检查和错误处理

  • 支持优先级控制和打断机制

  • 提供详细的播放状态反馈

核心代码

 1#include <aimdk_msgs/msg/tts_priority_level.hpp>
 2#include <aimdk_msgs/srv/play_tts.hpp>
 3#include <iostream>
 4#include <rclcpp/rclcpp.hpp>
 5#include <string>
 6
 7using PlayTTS = aimdk_msgs::srv::PlayTts;
 8
 9int main(int argc, char **argv) {
10  rclcpp::init(argc, argv);
11  auto node = rclcpp::Node::make_shared("play_tts_client_min");
12
13  const std::string service_name = "/aimdk_5Fmsgs/srv/PlayTts";
14  auto client = node->create_client<PlayTTS>(service_name);
15
16  // Get text to speak
17  std::string tts_text;
18  if (argc > 1) {
19    tts_text = argv[1];
20  } else {
21    std::cout << "Enter text to speak: ";
22    std::getline(std::cin, tts_text);
23    if (tts_text.empty()) {
24      tts_text = "Hello, I am AgiBot X2.";
25    }
26  }
27
28  auto req = std::make_shared<PlayTTS::Request>();
29  req->header.header.stamp = node->now();
30  req->tts_req.text = tts_text;
31  req->tts_req.domain = "demo_client"; // Required: identifies the caller
32  req->tts_req.trace_id =
33      "demo"; // Optional: request identifier for the TTS request
34  req->tts_req.is_interrupted =
35      true; // Required: whether to interrupt same-priority playback
36  req->tts_req.priority_weight = 0;
37  req->tts_req.priority_level.value = 6;
38
39  if (!client->wait_for_service(
40          std::chrono::duration_cast<std::chrono::seconds>(
41              std::chrono::seconds(5)))) {
42    RCLCPP_ERROR(node->get_logger(), "Service unavailable: %s",
43                 service_name.c_str());
44    rclcpp::shutdown();
45    return 1;
46  }
47
48  auto future = client->async_send_request(req);
49  if (rclcpp::spin_until_future_complete(
50          node, future,
51          std::chrono::duration_cast<std::chrono::seconds>(
52              std::chrono::seconds(10))) != rclcpp::FutureReturnCode::SUCCESS) {
53    RCLCPP_ERROR(node->get_logger(), "Call timed out");
54    rclcpp::shutdown();
55    return 1;
56  }
57
58  const auto resp = future.get();
59  if (resp->tts_resp.is_success) {
60    RCLCPP_INFO(node->get_logger(), "✅ TTS play request succeeded");
61  } else {
62    RCLCPP_ERROR(node->get_logger(), "❌ TTS play request failed");
63  }
64
65  rclcpp::shutdown();
66  return 0;
67}

使用说明

# 使用命令行参数播报文本(推荐)
ros2 run examples play_tts "你好,我是灵犀X2机器人"

# 或者不带参数运行,程序会提示用户输入
ros2 run examples play_tts

输出示例

[INFO] [play_tts_client_min]: ✅ 播报请求成功

注意事项

  • 确保TTS服务正常运行

  • 支持中文和英文文本播报

  • 优先级设置影响播放队列顺序

  • 打断功能可中断当前播放的语音

接口参考

  • 服务:/aimdk_5Fmsgs/srv/PlayTts

  • 消息:aimdk_msgs/srv/PlayTts

6.2.18 麦克风数据接收

该示例中用到了mic_receiver,通过订阅/agent/process_audio_output话题来接收机器人的降噪音频数据,支持内置麦克风和外置麦克风两种音频流,并根据VAD(语音活动检测)状态自动保存完整的语音片段为PCM文件。

功能特点:

  • 支持多音频流同时接收(内置麦克风 stream_id=1,外置麦克风 stream_id=2)

  • 基于VAD状态自动检测语音开始、处理中、结束

  • 自动保存完整语音片段为PCM格式文件

  • 按时间戳和音频流分类存储

  • 支持音频时长计算和统计信息输出

VAD状态说明:

  • 0: 无语音

  • 1: 语音开始

  • 2: 语音处理中

  • 3: 语音结束

  1#include <aimdk_msgs/msg/audio_vad_state_type.hpp>
  2#include <aimdk_msgs/msg/processed_audio_output.hpp>
  3#include <chrono>
  4#include <ctime>
  5#include <filesystem>
  6#include <fstream>
  7#include <iomanip>
  8#include <rclcpp/rclcpp.hpp>
  9#include <sstream>
 10#include <string>
 11#include <unordered_map>
 12#include <vector>
 13
 14namespace fs = std::filesystem;
 15
 16class AudioSubscriber : public rclcpp::Node {
 17public:
 18  AudioSubscriber() : rclcpp::Node("audio_subscriber") {
 19    // Audio buffers, stored separately by stream_id
 20    // stream_id -> buffer
 21    audio_buffers_ = {};
 22    recording_state_ = {};
 23
 24    audio_output_dir_ = "audio_recordings";
 25    fs::create_directories(audio_output_dir_);
 26
 27    // Note: deep queue to avoid missing data in a burst at start of VAD.
 28    auto qos = rclcpp::QoS(
 29        rclcpp::QoSInitialization::from_rmw(rmw_qos_profile_sensor_data));
 30    qos.keep_last(500).best_effort();
 31
 32    subscription_ =
 33        this->create_subscription<aimdk_msgs::msg::ProcessedAudioOutput>(
 34            "/agent/process_audio_output", qos,
 35            std::bind(&AudioSubscriber::audio_callback, this,
 36                      std::placeholders::_1));
 37
 38    RCLCPP_INFO(this->get_logger(),
 39                "Starting to subscribe to denoised audio data...");
 40  }
 41
 42private:
 43  void
 44  audio_callback(const aimdk_msgs::msg::ProcessedAudioOutput::SharedPtr msg) {
 45    try {
 46      uint32_t stream_id = msg->stream_id;
 47      uint8_t vad_state = msg->audio_vad_state.value;
 48      const std::vector<uint8_t> &audio_data = msg->audio_data;
 49
 50      static const std::unordered_map<uint8_t, std::string> vad_state_names = {
 51          {0, "No Speech"},
 52          {1, "Speech Start"},
 53          {2, "Speech Processing"},
 54          {3, "Speech End"}};
 55      static const std::unordered_map<uint32_t, std::string> stream_names = {
 56          {1, "Internal Microphone"}, {2, "External Microphone"}};
 57
 58      RCLCPP_INFO(this->get_logger(),
 59                  "Audio data received: stream_id=%u, vad_state=%u(%s), "
 60                  "audio_size=%zu bytes",
 61                  stream_id, vad_state,
 62                  vad_state_names.count(vad_state)
 63                      ? vad_state_names.at(vad_state).c_str()
 64                      : "Unknown State",
 65                  audio_data.size());
 66
 67      handle_vad_state(stream_id, vad_state, audio_data);
 68    } catch (const std::exception &e) {
 69      RCLCPP_ERROR(this->get_logger(), "Error processing audio message: %s",
 70                   e.what());
 71    }
 72  }
 73
 74  void handle_vad_state(uint32_t stream_id, uint8_t vad_state,
 75                        const std::vector<uint8_t> &audio_data) {
 76    // Initialize the buffer for this stream_id (if it does not exist)
 77    if (audio_buffers_.count(stream_id) == 0) {
 78      audio_buffers_[stream_id] = std::vector<uint8_t>();
 79      recording_state_[stream_id] = false;
 80    }
 81
 82    static const std::unordered_map<uint8_t, std::string> vad_state_names = {
 83        {0, "No Speech"},
 84        {1, "Speech Start"},
 85        {2, "Speech Processing"},
 86        {3, "Speech End"}};
 87    static const std::unordered_map<uint32_t, std::string> stream_names = {
 88        {1, "Internal Microphone"}, {2, "External Microphone"}};
 89
 90    RCLCPP_INFO(this->get_logger(), "[%s] VAD Atate: %s Audio Data: %zu bytes",
 91                stream_names.count(stream_id)
 92                    ? stream_names.at(stream_id).c_str()
 93                    : ("Unknown Stream " + std::to_string(stream_id)).c_str(),
 94                vad_state_names.count(vad_state)
 95                    ? vad_state_names.at(vad_state).c_str()
 96                    : ("Unknown State" + std::to_string(vad_state)).c_str(),
 97                audio_data.size());
 98
 99    // AUDIO_VAD_STATE_BEGIN
100    if (vad_state == 1) {
101      RCLCPP_INFO(this->get_logger(), "🎤 Speech detected - Start");
102      if (recording_state_[stream_id] == false) {
103        audio_buffers_[stream_id].clear();
104        recording_state_[stream_id] = true;
105      }
106      if (!audio_data.empty()) {
107        audio_buffers_[stream_id].insert(audio_buffers_[stream_id].end(),
108                                         audio_data.begin(), audio_data.end());
109      }
110
111      // AUDIO_VAD_STATE_PROCESSING
112    } else if (vad_state == 2) {
113      RCLCPP_INFO(this->get_logger(), "🔄 Speech Processing...");
114      if (recording_state_[stream_id] && !audio_data.empty()) {
115        audio_buffers_[stream_id].insert(audio_buffers_[stream_id].end(),
116                                         audio_data.begin(), audio_data.end());
117      }
118
119      // AUDIO_VAD_STATE_END
120    } else if (vad_state == 3) {
121      RCLCPP_INFO(this->get_logger(), "✅ Speech End");
122      if (recording_state_[stream_id] && !audio_data.empty()) {
123        audio_buffers_[stream_id].insert(audio_buffers_[stream_id].end(),
124                                         audio_data.begin(), audio_data.end());
125      }
126      if (recording_state_[stream_id] && !audio_buffers_[stream_id].empty()) {
127        save_audio_segment(audio_buffers_[stream_id], stream_id);
128      }
129      recording_state_[stream_id] = false;
130
131      // AUDIO_VAD_STATE_NONE
132    } else if (vad_state == 0) {
133      if (recording_state_[stream_id]) {
134        RCLCPP_INFO(this->get_logger(), "⏹️ Recording state reset");
135        recording_state_[stream_id] = false;
136      }
137    }
138
139    // Output the current buffer status.
140    size_t buffer_size = audio_buffers_[stream_id].size();
141    bool recording = recording_state_[stream_id];
142    RCLCPP_DEBUG(this->get_logger(),
143                 "[Stream %u] Buffer size: %zu bytes, Recording state: %s",
144                 stream_id, buffer_size, recording ? "true" : "false");
145  }
146
147  void save_audio_segment(const std::vector<uint8_t> &audio_data,
148                          uint32_t stream_id) {
149    if (audio_data.empty())
150      return;
151
152    // Get the current timestamp.
153    auto now = std::chrono::system_clock::now();
154    std::time_t t = std::chrono::system_clock::to_time_t(now);
155    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
156                  now.time_since_epoch()) %
157              1000;
158
159    std::ostringstream oss;
160    oss << std::put_time(std::localtime(&t), "%Y%m%d_%H%M%S") << "_"
161        << std::setw(3) << std::setfill('0') << ms.count();
162
163    // Create a subdirectory by stream_id.
164    fs::path stream_dir =
165        fs::path(audio_output_dir_) / ("stream_" + std::to_string(stream_id));
166    fs::create_directories(stream_dir);
167
168    static const std::unordered_map<uint32_t, std::string> stream_names = {
169        {1, "internal_mic"}, {2, "external_mic"}};
170    std::string stream_name = stream_names.count(stream_id)
171                                  ? stream_names.at(stream_id)
172                                  : ("stream_" + std::to_string(stream_id));
173    std::string filename = stream_name + "_" + oss.str() + ".pcm";
174    fs::path filepath = stream_dir / filename;
175
176    try {
177      std::ofstream ofs(filepath, std::ios::binary);
178      ofs.write(reinterpret_cast<const char *>(audio_data.data()),
179                audio_data.size());
180      ofs.close();
181      RCLCPP_INFO(this->get_logger(),
182                  "Audio segment saved: %s (size: %zu bytes)", filepath.c_str(),
183                  audio_data.size());
184
185      // Record audio file duration (assuming 16kHz, 16-bit, mono)
186      int sample_rate = 16000;
187      int bits_per_sample = 16;
188      int channels = 1;
189      int bytes_per_sample = bits_per_sample / 8;
190      size_t total_samples = audio_data.size() / (bytes_per_sample * channels);
191      double duration_seconds =
192          static_cast<double>(total_samples) / sample_rate;
193
194      RCLCPP_INFO(this->get_logger(),
195                  "Audio duration: %.2f seconds (%zu samples)",
196                  duration_seconds, total_samples);
197    } catch (const std::exception &e) {
198      RCLCPP_ERROR(this->get_logger(), "Failed to save audio file: %s",
199                   e.what());
200    }
201  }
202
203  // Member variables
204  std::unordered_map<uint32_t, std::vector<uint8_t>> audio_buffers_;
205  std::unordered_map<uint32_t, bool> recording_state_;
206  std::string audio_output_dir_;
207  rclcpp::Subscription<aimdk_msgs::msg::ProcessedAudioOutput>::SharedPtr
208      subscription_;
209};
210
211int main(int argc, char **argv) {
212  rclcpp::init(argc, argv);
213  auto node = std::make_shared<AudioSubscriber>();
214  RCLCPP_INFO(node->get_logger(),
215              "Listening for denoised audio data, press Ctrl+C to exit...");
216  rclcpp::spin(node);
217  rclcpp::shutdown();
218  return 0;
219}

使用说明:

  1. 运行节点后会自动创建audio_recordings目录

  2. 音频文件按stream_id分类存储:

    • stream_1/: 内置麦克风音频

    • stream_2/: 外置麦克风音频

  3. 文件命名格式:{stream_name}_{timestamp}.pcm

  4. 音频格式:16kHz, 16位, 单声道PCM

  5. 可通过以下命令播放保存的PCM文件:

    aplay -r 16000 -f S16_LE -c 1 external_mic_20250909_133649_738.pcm
    
  6. 配合唤醒词触发VAD

输出示例:

[INFO] 开始订阅降噪音频数据...
[INFO] 收到音频数据: stream_id=2, vad_state=1(语音开始), audio_size=320 bytes
[INFO] [外置麦克风] VAD状态: 语音开始 音频数据: 320 bytes
[INFO] 🎤 检测到语音开始
[INFO] 收到音频数据: stream_id=2, vad_state=2(语音处理中), audio_size=320 bytes
[INFO] [外置麦克风] VAD状态: 语音处理中 音频数据: 320 bytes
[INFO] 🔄 语音处理中...
[INFO] 收到音频数据: stream_id=2, vad_state=3(语音结束), audio_size=320 bytes
[INFO] [外置麦克风] VAD状态: 语音结束 音频数据: 320 bytes
[INFO] ✅ 语音结束
[INFO] 音频段已保存: audio_recordings/stream_2/external_mic_20250909_133649_738.pcm (大小: 960 bytes)
[INFO] 音频时长: 0.06 秒 (480 样本)

播放PCM音频文件示例 (Linux下使用aplay命令)

假设你已经录制并保存了音频文件 external_mic_20250909_151117_223.pcm, 可以通过如下命令进行播放:

aplay -r 16000 -f S16_LE -c 1 audio_recordings/stream_2/external_mic_20250909_151117_223.pcm

参数说明:

  • -r 16000 # 采样率16kHz

  • -f S16_LE # 16位小端格式

  • -c 1 # 单声道 你也可以用其他音频播放器(如Audacity)以原始PCM格式导入并播放。

注意:如果你保存的是内置麦克风音频,路径应为 audio_recordings/stream_1/internal_mic_xxx.pcm

6.2.19 表情控制

该示例中用到了play_emoji,通过该节点可实现表达指定的表情,用户可根据已有的表情列表来选择要变化的表情,具体表情列表可参考表情列表

  1#include "aimdk_msgs/srv/play_emoji.hpp"
  2#include "aimdk_msgs/msg/common_request.hpp"
  3#include "rclcpp/rclcpp.hpp"
  4#include <chrono>
  5#include <memory>
  6#include <signal.h>
  7#include <string>
  8
  9// Global variable used for signal handling
 10std::shared_ptr<rclcpp::Node> g_node = nullptr;
 11
 12// Signal handler function
 13void signal_handler(int signal) {
 14  if (g_node) {
 15    RCLCPP_INFO(g_node->get_logger(), "Received signal %d, shutting down...",
 16                signal);
 17    g_node.reset();
 18  }
 19  rclcpp::shutdown();
 20  exit(signal);
 21}
 22
 23class PlayEmojiClient : public rclcpp::Node {
 24public:
 25  PlayEmojiClient() : Node("play_emoji_client") {
 26    client_ = this->create_client<aimdk_msgs::srv::PlayEmoji>(
 27        "/face_ui_proxy/play_emoji");
 28    RCLCPP_INFO(this->get_logger(), "✅ PlayEmoji client node started.");
 29
 30    // Wait for the service to become available
 31    while (!client_->wait_for_service(std::chrono::seconds(2))) {
 32      RCLCPP_INFO(this->get_logger(), "⏳ Service unavailable, waiting...");
 33    }
 34    RCLCPP_INFO(this->get_logger(),
 35                "🟢 Service available, ready to send request.");
 36  }
 37
 38  bool send_request(uint8_t emoji, uint8_t mode, int32_t priority) {
 39    try {
 40      auto request = std::make_shared<aimdk_msgs::srv::PlayEmoji::Request>();
 41
 42      request->emotion_id = emoji;
 43      request->mode = mode;
 44      request->priority = priority;
 45
 46      RCLCPP_INFO(
 47          this->get_logger(),
 48          "📨 Sending request to play emoji: id=%hhu, mode=%hhu, priority=%d",
 49          emoji, mode, priority);
 50
 51      const std::chrono::milliseconds timeout(250);
 52      for (int i = 0; i < 8; i++) {
 53        request->header.header.stamp = this->now();
 54        auto future = client_->async_send_request(request);
 55        auto retcode = rclcpp::spin_until_future_complete(shared_from_this(),
 56                                                          future, timeout);
 57        if (retcode != rclcpp::FutureReturnCode::SUCCESS) {
 58          // retry as remote peer is NOT handled well by ROS
 59          RCLCPP_INFO(this->get_logger(), "trying ... [%d]", i);
 60          continue;
 61        }
 62        // future.done
 63        auto response = future.get();
 64        if (response->success) {
 65          RCLCPP_INFO(this->get_logger(),
 66                      "✅ Request to play emoji recorded successfully: %s",
 67                      response->message.c_str());
 68          return true;
 69        } else {
 70          RCLCPP_ERROR(this->get_logger(),
 71                       "❌ Failed to record play-emoji request: %s",
 72                       response->message.c_str());
 73          return false;
 74        }
 75      }
 76      RCLCPP_ERROR(this->get_logger(), "❌ Service call failed or timed out.");
 77      return false;
 78    } catch (const std::exception &e) {
 79      RCLCPP_ERROR(this->get_logger(), "Exception occurred: %s", e.what());
 80      return false;
 81    }
 82  }
 83
 84private:
 85  rclcpp::Client<aimdk_msgs::srv::PlayEmoji>::SharedPtr client_;
 86};
 87
 88int main(int argc, char **argv) {
 89  try {
 90    rclcpp::init(argc, argv);
 91
 92    // Set up signal handlers
 93    signal(SIGINT, signal_handler);
 94    signal(SIGTERM, signal_handler);
 95
 96    int32_t priority = 10;
 97
 98    int emotion = 1; // Expression type, 1 means Blink
 99    std::cout
100        << "Enter expression ID: 1-Blink, 60-Bored, 70-Abnormal, 80-Sleeping, "
101           "90-Happy, 190-Very Angry, 200-Adoration"
102        << std::endl;
103    std::cin >> emotion;
104
105    int mode = 1; // Playback mode, 1 means play once, 2 means loop
106    std::cout << "Enter play mode (1: once, 2: loop): ";
107    std::cin >> mode;
108    if (mode < 1 || mode > 2) {
109      RCLCPP_ERROR(rclcpp::get_logger("main"), "Invalid play mode: %d", mode);
110      rclcpp::shutdown();
111      return 1;
112    }
113
114    g_node = std::make_shared<PlayEmojiClient>();
115    auto client = std::dynamic_pointer_cast<PlayEmojiClient>(g_node);
116
117    if (client) {
118      client->send_request(emotion, mode, priority);
119    }
120
121    // Clean up resources
122    g_node.reset();
123    rclcpp::shutdown();
124
125    return 0;
126  } catch (const std::exception &e) {
127    RCLCPP_ERROR(rclcpp::get_logger("main"),
128                 "Program exited with exception: %s", e.what());
129    return 1;
130  }
131}

6.2.20 LED灯带控制

功能说明:演示如何控制机器人的LED灯带,支持多种显示模式和自定义颜色。

核心代码

  1#include <aimdk_msgs/msg/common_request.hpp>
  2#include <aimdk_msgs/srv/led_strip_command.hpp>
  3#include <chrono>
  4#include <memory>
  5#include <rclcpp/rclcpp.hpp>
  6#include <signal.h>
  7#include <string>
  8
  9std::shared_ptr<rclcpp::Node> g_node = nullptr;
 10
 11void signal_handler(int signal) {
 12  if (g_node) {
 13    RCLCPP_INFO(g_node->get_logger(), "Received signal %d, shutting down...",
 14                signal);
 15    g_node.reset();
 16  }
 17  rclcpp::shutdown();
 18  exit(signal);
 19}
 20
 21class PlayLightsClient : public rclcpp::Node {
 22public:
 23  PlayLightsClient() : Node("play_lights_client") {
 24    client_ = this->create_client<aimdk_msgs::srv::LedStripCommand>(
 25        "/aimdk_5Fmsgs/srv/LedStripCommand");
 26    RCLCPP_INFO(this->get_logger(), "✅ PlayLights client node started.");
 27
 28    // Wait for the service to become available
 29    while (!client_->wait_for_service(std::chrono::seconds(2))) {
 30      RCLCPP_INFO(this->get_logger(), "⏳ Service unavailable, waiting...");
 31    }
 32    RCLCPP_INFO(this->get_logger(),
 33                "🟢 Service available, ready to send request.");
 34  }
 35
 36  bool send_request(uint8_t led_mode, uint8_t r, uint8_t g, uint8_t b) {
 37    try {
 38      auto request =
 39          std::make_shared<aimdk_msgs::srv::LedStripCommand::Request>();
 40
 41      request->led_strip_mode = led_mode;
 42      request->r = r;
 43      request->g = g;
 44      request->b = b;
 45
 46      RCLCPP_INFO(this->get_logger(),
 47                  "📨 Sending request to control led strip: mode=%hhu, "
 48                  "RGB=(%hhu, %hhu, %hhu)",
 49                  led_mode, r, g, b);
 50
 51      // LED strip is slow to response (up to ~5s)
 52      const std::chrono::milliseconds timeout(5000);
 53      for (int i = 0; i < 4; i++) {
 54        request->request.header.stamp = this->now();
 55        auto future = client_->async_send_request(request);
 56        auto retcode = rclcpp::spin_until_future_complete(
 57            this->shared_from_this(), future, timeout);
 58
 59        if (retcode != rclcpp::FutureReturnCode::SUCCESS) {
 60          // retry as remote peer is NOT handled well by ROS
 61          RCLCPP_INFO(this->get_logger(), "trying ... [%d]", i);
 62          continue;
 63        }
 64        // future.done
 65        auto response = future.get();
 66        if (response->status_code == 0) {
 67          RCLCPP_INFO(this->get_logger(),
 68                      "✅ LED strip command sent successfully.");
 69          return true;
 70        } else {
 71          RCLCPP_ERROR(this->get_logger(),
 72                       "❌ LED strip command failed with status: %d",
 73                       response->status_code);
 74          return false;
 75        }
 76      }
 77      RCLCPP_ERROR(this->get_logger(), "❌ Service call failed or timed out.");
 78      return false;
 79    } catch (const std::exception &e) {
 80      RCLCPP_ERROR(this->get_logger(), "Exception occurred: %s", e.what());
 81      return false;
 82    }
 83  }
 84
 85private:
 86  rclcpp::Client<aimdk_msgs::srv::LedStripCommand>::SharedPtr client_;
 87};
 88
 89int main(int argc, char **argv) {
 90  try {
 91    rclcpp::init(argc, argv);
 92    signal(SIGINT, signal_handler);
 93    signal(SIGTERM, signal_handler);
 94
 95    g_node = std::make_shared<PlayLightsClient>();
 96    auto client_node = std::dynamic_pointer_cast<PlayLightsClient>(g_node);
 97
 98    int led_mode = 0;          // LED Strip Mode
 99    int r = 255, g = 0, b = 0; // RGB values
100
101    std::cout << "=== LED Strip Control Example ===" << std::endl;
102    std::cout << "Select LED strip mode:" << std::endl;
103    std::cout << "0 - Steady On" << std::endl;
104    std::cout << "1 - Breathing (4s period, sinusoidal brightness)"
105              << std::endl;
106    std::cout << "2 - Blinking (1s period, 0.5s on, 0.5s off)" << std::endl;
107    std::cout << "3 - Flow (2s period, lights turn on left to right)"
108              << std::endl;
109    std::cout << "Enter mode (0-3): ";
110    std::cin >> led_mode;
111
112    std::cout << "\nSet RGB color values (0-255):" << std::endl;
113    std::cout << "Red component (R): ";
114    std::cin >> r;
115    std::cout << "Green component (G): ";
116    std::cin >> g;
117    std::cout << "Blue component (B): ";
118    std::cin >> b;
119
120    // clamp mode to range 0-3
121    led_mode = std::max(0, std::min(3, led_mode));
122    // clamp r/g/b to range 0-255
123    r = std::max(0, std::min(255, r));
124    g = std::max(0, std::min(255, g));
125    b = std::max(0, std::min(255, b));
126
127    if (client_node) {
128      client_node->send_request(led_mode, r, g, b);
129    }
130
131    g_node.reset();
132    rclcpp::shutdown();
133
134    return 0;
135  } catch (const std::exception &e) {
136    RCLCPP_ERROR(rclcpp::get_logger("main"),
137                 "Program terminated with exception: %s", e.what());
138    return 1;
139  }
140}

使用说明

# 构建
colcon build --packages-select examples

# 运行
ros2 run examples play_lights

输出示例

=== LED灯带控制示例 ===
请选择灯带模式:
0 - 常亮模式
1 - 呼吸模式 (4s周期,亮度正弦变化)
2 - 闪烁模式 (1s周期,0.5s亮,0.5s灭)
3 - 流水模式 (2s周期,从左到右依次点亮)
请输入模式 (0-3): 1

请设置RGB颜色值 (0-255):
红色分量 (R): 255
绿色分量 (G): 0
蓝色分量 (B): 0

发送LED控制命令...
模式: 1, 颜色: RGB(255, 0, 0)
✅ LED strip command sent successfully

技术特点

  • 支持4种LED显示模式

  • RGB颜色自定义

  • 异步服务调用

  • 输入参数验证

  • 友好的用户交互界面