从您自己的节点记录一个包 [待校准@7994]

Goal目标: Record将数据从您自己的节点记录到包中。 [待校准@7995]

Tutorial教程级别: Advanced高级 [待校准@7643]

Time时间: 20 20分钟 [待校准@7181]

背景

[需手动修复的语法]``rosbag2`` doesn't just provide the ros2 bag command线工具。它还提供了一个API,用于从您自己的源文件读取和写入包。这使您可以订阅某个话题,并将收到的数据保存到包中,同时对该数据执行您选择的任何其他处理。 [待校准@7996]

先决条件

您应该安装 rosbag2 包作为常规ROS 2设置的一部分。 [待校准@7997]

如果您是从Linux上的Debian包安装的,则默认情况下可能会安装它。如果不是,您可以使用此命令安装它。 [待校准@7998]

sudo apt install ros-foxy-rosbag2

本教程讨论使用ROS 2包,包括从终端。你应该已经完成了 basic ROS 2 bag tutorial[待校准@7999]

任务

1创建包

打开一个新的终端和 source your ROS 2 installation ,这样 ros2 命令就可以工作了。 [待校准@8000]

导航到 dev_ws 目录创建 previous tutorial 。导航到 dev_ws/src 目录并创建一个新包: [待校准@8001]

ros2 pkg create --build-type ament_cmake bag_recorder_nodes --dependencies rclcpp rosbag2_cpp example_interfaces

您的终端将返回一条消息,验证您的包 bag_recorder_nodes 及其所有必要的文件和文件夹的创建。 --dependencies 参数将自动调用y向 package.xmlCMakeLists.txt 添加必要的依赖线。在这种情况下,包装将使用 rosbag2_cpp 包装以及 rclcpp 包装。本教程的后面部分还需要依赖于 example_interfaces 包。 [待校准@8002]

1.1更新 package.xml [待校准@8003]

因为您在包创建过程中使用了 --dependencies 选项,所以您不必手动向 package.xmlCMakeLists.txt 添加依赖项。但是,与往常一样,请确保将描述、维护人员电子邮件和姓名以及许可信息添加到 package.xml[待校准@8004]

<description>C++ bag writing tutorial</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache License 2.0</license>

2编写cnode节点 [待校准@8005]

dev_ws/src/bag_recorder_nodes/src 目录内,创建一个调用 simple_bag_recorder.cpp 的新文件,并将以下代码粘贴到其中。 [待校准@8006]

#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>

#include <rosbag2_cpp/typesupport_helpers.hpp>
#include <rosbag2_cpp/writer.hpp>
#include <rosbag2_cpp/writers/sequential_writer.hpp>
#include <rosbag2_storage/serialized_bag_message.hpp>

using std::placeholders::_1;

class SimpleBagRecorder : public rclcpp::Node
{
public:
  SimpleBagRecorder()
  : Node("simple_bag_recorder")
  {
    const rosbag2_cpp::StorageOptions storage_options({"my_bag", "sqlite3"});
    const rosbag2_cpp::ConverterOptions converter_options(
      {rmw_get_serialization_format(),
       rmw_get_serialization_format()});
    writer_ = std::make_unique<rosbag2_cpp::writers::SequentialWriter>();

    writer_->open(storage_options, converter_options);

    writer_->create_topic(
      {"chatter",
       "std_msgs/msg/String",
       rmw_get_serialization_format(),
       ""});

    subscription_ = create_subscription<std_msgs::msg::String>(
      "chatter", 10, std::bind(&SimpleBagRecorder::topic_callback, this, _1));
  }

private:
  void topic_callback(std::shared_ptr<rclcpp::SerializedMessage> msg) const
  {
    auto bag_message = std::make_shared<rosbag2_storage::SerializedBagMessage>();

    bag_message->serialized_data = std::shared_ptr<rcutils_uint8_array_t>(
      new rcutils_uint8_array_t,
      [this](rcutils_uint8_array_t *msg) {
        auto fini_return = rcutils_uint8_array_fini(msg);
        delete msg;
        if (fini_return != RCUTILS_RET_OK) {
          RCLCPP_ERROR(get_logger(),
            "Failed to destroy serialized message %s", rcutils_get_error_string().str);
        }
      });
    *bag_message->serialized_data = msg->release_rcl_serialized_message();

    bag_message->topic_name = "chatter";
    if (rcutils_system_time_now(&bag_message->time_stamp) != RCUTILS_RET_OK) {
      RCLCPP_ERROR(get_logger(), "Error getting current time: %s",
        rcutils_get_error_string().str);
    }

    writer_->write(bag_message);
  }

  rclcpp::Subscription<rclcpp::SerializedMessage>::SharedPtr subscription_;
  std::unique_ptr<rosbag2_cpp::writers::SequentialWriter> writer_;
};

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

2.1检查代码 [待校准@8007]

顶部的 “# include” 语句是包依赖项。注意包含来自 rosbag2_cpp 包的标题的功能和必要的结构来处理包文件。 [待校准@8008]

在类构造函数中,我们首先创建将用于写入包的writer对象。我们必须为袋子提供存储选项。这些指定了袋子的名称 ( my_bag ) 和格式 ( sqlite3 )。我们还必须提供转换选项,这些选项指定输入到编写器中的数据将如何序列化,以及在写入包时如何序列化相同的数据。在大多数情况下,您可以将它们指定为相同的值,因为不需要转换序列化格式。我们使用 rmw_get_serialization_format() 函数来检索底层中间件使用的序列化格式。这是将接收数据的序列化格式,我们将其存储在包中的格式也是如此。 [待校准@8009]

最后,第三行创建writer对象。我们创建了一个 SequentialWriter ,这是最简单的作家对象。它不会执行高级操作,例如在将数据写入包时压缩数据。 [待校准@8010]

rosbag2_cpp::StorageOptions storage_options({"my_bag", "sqlite3"});
rosbag2_cpp::ConverterOptions converter_options({rmw_get_serialization_format(), rmw_get_serialization_format()});
writer_ = std::make_unique<rosbag2_cpp::writers::SequentialWriter>();

现在我们有了一个作家对象,我们可以用它打开袋子。 [待校准@8011]

writer_->open(storage_options, converter_options);

下一步是通知作者我们将写给书包的每个话题。这是通过调用 create_topic 并传递 “rosbag2_storage::话题metadata” 结构的实例来完成的。在这里,我们使用现代的csyntax语法在适当的位置构造此结构的实例,而不是单独创建它并将其传递。存储在 “rosbag2_storage::话题metadata” 结构中的参数为: [待校准@8012]

  • 话题的名称。请注意,这不需要与接收数据的话题相同。 [待校准@8013]

  • 话题中的数据类型。这必须与存储在包中的数据类型相同。 [待校准@8014]

  • 数据的序列化格式。如前所述,我们仅使用与底层中间件相同的序列化格式。 [待校准@8015]

  • 为话题指定的任何QoS设置。这些必须以YAML格式指定。将其保留为空string将使用系统默认值。 [待校准@8016]

writer_->create_topic({"chatter", "std_msgs/msg/String", rmw_get_serialization_format(), ""});

现在,通过设置编写器来记录我们传递给它的数据,我们创建一个订阅并为它指定一个调用。我们将在调用中将数据写入包中。 [待校准@8017]

subscription_ = create_subscription<std_msgs::msg::String>(
  "chatter", 10, std::bind(&SimpleBagRecorder::topic_callback, this, _1));

调用back本身不同于典型的调用back。而不是接收话题的数据类型的实例,而是接收 “rclcpp:: serializedmessage”。我们这样做有两个原因。 [待校准@8018]

  1. 在写入包之前,需要对消息数据进行序列化,因此,与其在接收数据时取消序列化,然后重新序列化,我们要求ROS按原样给我们序列化的消息。 [待校准@8019]

  2. writer API需要一个序列化消息,因此通过向ROS请求序列化消息,我们节省了自己对数据进行序列化的工作。 [待校准@8020]

void topic_callback(std::shared_ptr<rclcpp::SerializedMessage> msg) const
{

在订阅调用中,首先要做的是创建 “rosbag2_storage:: serializedbagmessage” 的实例。这是表示包中的单个数据样本的数据类型。 [待校准@8021]

auto bag_message = std::make_shared<rosbag2_storage::SerializedBagMessage>();

由于直接处理来自中间件的序列化数据的内存管理要求,下一个代码块有些复杂。序列化数据的内存归 SerializedMessage 对象所有,但我们将传递给包的 SerializedBagMessage 对象必须拥有内存。否则,内存可能会超出范围,并在写入包之前被删除,从而导致内存访问错误。为了防止这种情况,我们在 SerializedMessage 物体上调用 release_rcl_serialized_message() 。这使得它释放对记忆的所有权,允许 SerializedBagMessage 获得所有权。 [待校准@8022]

然而,我们也需要确保 SerializedBagMessage 物体在被清理时会正确地删除内存。这是通过在创建 bag_message 实例的 serialized_data 成员时提供自定义删除器函数来实现的。这是将lambda函数传递到 std::shared_ptr<rcutils_uint8_array_t> 对象的构造函数中的目的 (即 bag_message 物体的 serialized_data 成员)。当 shared_ptr 超出范围时,将调用lambda函数并清理内存。 [待校准@8023]

bag_message->serialized_data = std::shared_ptr<rcutils_uint8_array_t>(
  new rcutils_uint8_array_t,
  [this](rcutils_uint8_array_t *msg) {
    auto fini_return = rcutils_uint8_array_fini(msg);
    delete msg;
    if (fini_return != RCUTILS_RET_OK) {
      RCLCPP_ERROR(get_logger(),
        "Failed to destroy serialized message %s", rcutils_get_error_string().str);
    }
  });
*bag_message->serialized_data = msg->release_rcl_serialized_message();

下一行用来告诉作者这个样本是为了什么话题。这是必要的,因为序列化消息不包含任何类型信息,并且可能会错误地写入包中的任何话题。 [待校准@8024]

bag_message->topic_name = "chatter";

时间戳的消息必须设置在 time_stamp 成员 bag_message 对象。这可以是任何适合您的数据的值,但是两个共同的值是数据生成的时间 (如果知道) 和接收时间。第二种选择,即接收时间,在此使用。 [待校准@8025]

if (rcutils_system_time_now(&bag_message->time_stamp) != RCUTILS_RET_OK) {
  RCLCPP_ERROR(get_logger(), "Error getting current time: %s",
    rcutils_get_error_string().str);
}

调用back的最后一步是将数据传递给writer对象,以便将其写入bag。 [待校准@8026]

writer_->write(bag_message);

该类包含两个成员变量。 [待校准@8027]

  1. 订阅对象。请注意,模板参数是调用back的类型,而不是话题的类型。在这种情况下,调用back会收到一个 “rclcpp:: serializedmessage” 共享指针,因此这是模板参数必须的。 [待校准@8028]

  2. 指向用于写入包的writer对象的托管指针。请注意,此处使用的编写器类型为 “rosbag2_cpp:: 编写器:: sequentialwriter”。其他作家可能有不同的行为。 [待校准@8029]

rclcpp::Subscription<rclcpp::SerializedMessage>::SharedPtr subscription_;
std::unique_ptr<rosbag2_cpp::writers::SequentialWriter> writer_;

该文件以用于创建节点实例并开始ROS处理的 main 函数结束。 [待校准@8030]

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

2.2添加可执行文件 [待校准@8031]

现在打开 CMakeLists.txt 文件。在 find_package(rosbag2_cpp REQUIRED) 依赖项下添加以下代码行。 [待校准@8032]

add_executable(simple_bag_recorder src/simple_bag_recorder.cpp)
ament_target_dependencies(simple_bag_recorder rclcpp rosbag2_cpp)

install(TARGETS
  simple_bag_recorder
  DESTINATION lib/${PROJECT_NAME}
)

3构建和运行 [待校准@8033]

导航回工作区的根, dev_ws ,并构建新的包。 [待校准@8034]

colcon build --packages-select bag_recorder_nodes

打开一个新终端,导航到 dev_ws ,然后源文件安装文件。 [待校准@8035]

source install/setup.bash

为包创建一个目录。此目录将包含构成单个包的所有文件。 [待校准@8036]

mkdir my_bag

(如果 my_bag 目录已经存在,您必须先删除它,然后再重新创建它。) [待校准@8037]

现在运行节点: [待校准@7899]

ros2 run bag_recorder_nodes simple_bag_recorder

打开第二个终端并运行 talker 示例节点。 [待校准@8038]

ros2 run demo_nodes_cpp talker

这将开始发布关于 chatter 话题的数据。当写袋节点接收到这些数据时,它会将其写入 my_bag 袋。 [待校准@8039]

终止两个节点。然后,在一个终端启动 listener 示例节点。 [待校准@8040]

ros2 run demo_nodes_cpp listener

在另一个终端,使用 ros2 bag 播放节点录制的包。 [待校准@8041]

ros2 bag play my_bag

你会看到来自袋子的信息被 listener 节点接收。 [待校准@8042]

4记录来自节点的合成数据 [待校准@8043]

任何数据都可以记录到一个袋子里,而不仅仅是通过一个话题接收到的数据。从您自己的节点写入包的常见用例是生成和存储合成数据。在本节中,您将学习如何编写生成一些数据并将其存储在包中的节点。我们将例程指定两种方法来执行此操作。第一个使用带有计时器的节点; 如果数据生成在节点外部,例如直接从硬件读取数据 (例如g.照相机)。第二种方法不使用节点; 当您不需要使用ROS基础结构中的任何功能时,可以使用此方法。 [待校准@8044]

4.1编写cnode节点 [待校准@8045]

dev_ws/src/bag_recorder_nodes/src 目录内,创建一个调用 data_generator_node.cpp 的新文件,并将以下代码粘贴到其中。 [待校准@8046]

#include <chrono>

#include <example_interfaces/msg/int32.hpp>
#include <rclcpp/rclcpp.hpp>
#include <rclcpp/serialization.hpp>

#include <rosbag2_cpp/writer.hpp>
#include <rosbag2_cpp/writers/sequential_writer.hpp>
#include <rosbag2_storage/serialized_bag_message.hpp>

using namespace std::chrono_literals;

class DataGenerator : public rclcpp::Node
{
public:
  DataGenerator()
  : Node("data_generator")
  {
    data.data = 0;
    const rosbag2_cpp::StorageOptions storage_options({"timed_synthetic_bag", "sqlite3"});
    const rosbag2_cpp::ConverterOptions converter_options(
      {rmw_get_serialization_format(),
       rmw_get_serialization_format()});
    writer_ = std::make_unique<rosbag2_cpp::writers::SequentialWriter>();

    writer_->open(storage_options, converter_options);

    writer_->create_topic(
      {"synthetic",
       "example_interfaces/msg/Int32",
       rmw_get_serialization_format(),
       ""});

    timer_ = create_wall_timer(1s, std::bind(&DataGenerator::timer_callback, this));
  }

private:
  void timer_callback()
  {
    auto serializer = rclcpp::Serialization<example_interfaces::msg::Int32>();
    auto serialized_message = rclcpp::SerializedMessage();
    serializer.serialize_message(&data, &serialized_message);

    auto bag_message = std::make_shared<rosbag2_storage::SerializedBagMessage>();

    bag_message->serialized_data = std::shared_ptr<rcutils_uint8_array_t>(
      new rcutils_uint8_array_t,
      [this](rcutils_uint8_array_t *msg) {
        auto fini_return = rcutils_uint8_array_fini(msg);
        delete msg;
        if (fini_return != RCUTILS_RET_OK) {
          RCLCPP_ERROR(get_logger(),
            "Failed to destroy serialized message %s", rcutils_get_error_string().str);
        }
      });
    *bag_message->serialized_data = serialized_message.release_rcl_serialized_message();

    bag_message->topic_name = "synthetic";
    if (rcutils_system_time_now(&bag_message->time_stamp) != RCUTILS_RET_OK) {
      RCLCPP_ERROR(get_logger(), "Error getting current time: %s",
        rcutils_get_error_string().str);
    }

    writer_->write(bag_message);
    ++data.data;
  }

  rclcpp::TimerBase::SharedPtr timer_;
  std::unique_ptr<rosbag2_cpp::writers::SequentialWriter> writer_;
  example_interfaces::msg::Int32 data;
};

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

4.2检查代码 [待校准@8047]

此代码的大部分与第一个示例相同。这里描述了重要的区别。 [待校准@8048]

首先,更改包的名称。 [待校准@8049]

rosbag2_cpp::StorageOptions storage_options({"timed_synthetic_bag", "sqlite3"});

将存储的话题名称和数据类型也不同,因此需要告知作者这一点。 [待校准@8050]

writer_->create_topic({"synthetic", "example_interfaces/msg/Int32", rmw_get_serialization_format(), ""});

这个节点有一个计时器,而不是订阅一个话题。计时器以一秒钟的周期触发,并在给定成员函数触发时调用s。 [待校准@8051]

timer_ = create_wall_timer(1s, std::bind(&DataGenerator::timer_callback, this));

在计时器调用中,我们生成 (或以其他方式获取,例如从连接到某些硬件的串行端口读取) 我们希望存储在包中的数据。此示例与先前示例之间的重要区别在于,数据尚未序列化。因为包编写器需要序列化数据,所以我们必须先序列化它。这可以使用 “rclcpp:: serialization” 类来完成。 [待校准@8052]

auto serializer = rclcpp::Serialization<example_interfaces::msg::Int32>();
auto serialized_message = rclcpp::SerializedMessage();
serializer.serialize_message(&data, &serialized_message);

调用back中的其余代码是相同的,稍作修改以考虑到话题名称和数据类型的差异,并在每次执行调用back时增加数据。 [待校准@8053]

4.3添加可执行文件 [待校准@8054]

打开 CMakeLists.txt 文件并添加以下行后先前添加 (具体调用y,后 install(TARGETS ...) 微距调用)。 [待校准@8055]

add_executable(data_generator_node src/data_generator_node.cpp)
ament_target_dependencies(data_generator_node rclcpp rosbag2_cpp example_interfaces)

install(TARGETS
  data_generator_node
  DESTINATION lib/${PROJECT_NAME}
)

4.4构建和运行 [待校准@8056]

导航回工作区的根, dev_ws ,并构建您的包。 [待校准@8057]

colcon build --packages-select bag_recorder_nodes

打开一个新终端,导航到 dev_ws ,然后源文件安装文件。 [待校准@8035]

source install/setup.bash

为包创建一个目录。此目录将包含构成单个包的所有文件。 [待校准@8036]

mkdir timed_synthetic_bag

(如果 timed_synthetic_bag 目录已经存在,您必须先删除它,然后再重新创建它。) [待校准@8058]

现在运行节点: [待校准@7899]

ros2 run bag_recorder_nodes data_generator_node

等待30秒左右,然后用 ctrl-c 终止节点。接下来,回放创建的包。 [待校准@8059]

ros2 bag play timed_synthetic_bag

打开第二个终端,回应 /synthetic 话题。 [待校准@8060]

ros2 topic echo /synthetic

您将看到生成并存储在袋中的数据以每秒一条消息的速度打印到控制台。 [待校准@8061]

5记录来自可执行文件的合成数据 [待校准@8062]

现在,您可以创建一个包来存储来自除话题以外的源文件的数据,您将学习如何从非节点可执行文件生成和记录合成数据。这种方法的优点是代码更简单,可快速创建大量数据。 [待校准@8063]

5.1编写cexecutable可执行文件 [待校准@8064]

dev_ws/src/bag_recorder_nodes/src 目录内,创建一个调用 data_generator_executable.cpp 的新文件,并将以下代码粘贴到其中。 [待校准@8065]

#include <iostream>

#include <rclcpp/rclcpp.hpp>
#include <rclcpp/serialization.hpp>
#include <example_interfaces/msg/int32.hpp>

#include <rosbag2_cpp/writer.hpp>
#include <rosbag2_cpp/writers/sequential_writer.hpp>
#include <rosbag2_storage/serialized_bag_message.hpp>

int main(int, char**)
{
  example_interfaces::msg::Int32 data;
  data.data = 0;
  const rosbag2_cpp::StorageOptions storage_options({"big_synthetic_bag", "sqlite3"});
  const rosbag2_cpp::ConverterOptions converter_options(
    {rmw_get_serialization_format(),
     rmw_get_serialization_format()});
  std::unique_ptr<rosbag2_cpp::writers::SequentialWriter> writer_ =
    std::make_unique<rosbag2_cpp::writers::SequentialWriter>();

  writer_->open(storage_options, converter_options);

  writer_->create_topic(
    {"synthetic",
     "example_interfaces/msg/Int32",
     rmw_get_serialization_format(),
     ""});

  rcutils_time_point_value_t time_stamp;
  if (rcutils_system_time_now(&time_stamp) != RCUTILS_RET_OK) {
    std::cerr << "Error getting current time: " <<
      rcutils_get_error_string().str;
    return 1;
  }
  for (int32_t ii = 0; ii < 100; ++ii) {
    auto serializer = rclcpp::Serialization<example_interfaces::msg::Int32>();
    auto serialized_message = rclcpp::SerializedMessage();
    serializer.serialize_message(&data, &serialized_message);

    auto bag_message = std::make_shared<rosbag2_storage::SerializedBagMessage>();

    bag_message->serialized_data = std::shared_ptr<rcutils_uint8_array_t>(
      new rcutils_uint8_array_t,
      [](rcutils_uint8_array_t *msg) {
        auto fini_return = rcutils_uint8_array_fini(msg);
        delete msg;
        if (fini_return != RCUTILS_RET_OK) {
          std::cerr << "Failed to destroy serialized message " <<
            rcutils_get_error_string().str;
        }
      });
    *bag_message->serialized_data = serialized_message.release_rcl_serialized_message();

    bag_message->topic_name = "synthetic";
    bag_message->time_stamp = time_stamp;

    writer_->write(bag_message);
    ++data.data;
    time_stamp += 1000000000;
  }

  return 0;
}

5.2检查代码 [待校准@8066]

该样本与先前样本的比较将揭示它们并没有什么不同。唯一显著的区别是使用for循环来驱动数据生成,而不是计时器。 [待校准@8067]

请注意,我们现在也在为数据生成时间戳,而不是依赖于每个样本的当前系统时间。时间戳可以是您需要的任何值。数据将以这些时间戳给出的速率回放,因此这是控制样本默认回放速度的有用方法。还请注意,虽然每个样本之间的间隔是整整一秒的时间,但此可执行文件不需要在每个样本之间等待一秒钟。这使我们能够在比回放所需的更短的时间内生成大量覆盖时间跨度的数据。 [待校准@8068]

rcutils_time_point_value_t time_stamp;
if (rcutils_system_time_now(&time_stamp) != RCUTILS_RET_OK) {
  std::cerr << "Error getting current time: " <<
    rcutils_get_error_string().str;
  return 1;
}
for (int32_t ii = 0; ii < 100; ++ii) {
  ...
  bag_message->time_stamp = time_stamp;
  ...
  time_stamp += 1000000000;
}

5.3添加可执行文件 [待校准@8069]

打开 CMakeLists.txt 文件,并在之前添加的行之后添加以下行。 [待校准@8070]

add_executable(data_generator_executable src/data_generator_executable.cpp)
ament_target_dependencies(data_generator_executable rclcpp rosbag2_cpp example_interfaces)

install(TARGETS
  data_generator_executable
  DESTINATION lib/${PROJECT_NAME}
)

5.4构建和运行 [待校准@8071]

导航回工作区的根, dev_ws ,并构建您的包。 [待校准@8057]

colcon build --packages-select bag_recorder_nodes

打开终端,导航到 dev_ws ,然后源文件安装文件。 [待校准@8072]

source install/setup.bash

为包创建一个目录。此目录将包含构成单个包的所有文件。 [待校准@8036]

mkdir big_synthetic_bag

(如果 big_synthetic_bag 目录已经存在,您必须先删除它,然后再重新创建它。) [待校准@8073]

现在运行可执行文件: [待校准@8074]

ros2 run bag_recorder_nodes data_generator_executable

请注意,可执行文件运行和完成非常快。 [待校准@8075]

现在回放创建的包。 [待校准@8076]

ros2 bag play big_synthetic_bag

打开第二个终端,回应 /synthetic 话题。 [待校准@8060]

ros2 topic echo /synthetic

您将看到生成并存储在袋中的数据以每秒一条消息的速度打印到控制台。即使袋子生成得很快,它仍然会按照时间戳显示的速度回放。 [待校准@8077]

总结

您创建了一个节点,将它接收到的关于一个话题的数据记录到一个袋子中。您测试了使用节点记录一个包,并通过回放包验证了数据记录。然后,您继续创建一个节点和一个可执行文件,以生成合成数据并将其存储在袋子中。 [待校准@8078]