警告
您正在阅读的 ROS 2 文档版本已达到 EOL(生命周期结束),不再受官方支持。如果您想了解最新信息,请访问 Jazzy.
从节点记录一个袋子 (C++)
目标 将自己节点的数据记录到袋子中。
辅导水平: 高级
时间 20 分钟
背景介绍
rosbag2
不只是提供 玫瑰2 背包
命令行工具。它还提供了一个应用程序接口,用于从自己的源代码中读取和写入数据包。这样,您就可以订阅一个主题,并在对数据进行其他处理的同时,将接收到的数据保存到数据包中。
先决条件
您应该有 rosbag2
作为 ROS 2 常规设置的一部分安装的软件包。
如果你在 Linux 上安装了 Debian 软件包,它可能已经默认安装。如果没有,可以使用此命令进行安装。
sudo apt install ros-foxy-rosbag2
本教程讨论如何使用 ROS 2 工具包,包括从终端使用。您应该已经完成了 ROS 2 工具包基础教程.
任务
1 创建软件包
打开一个新终端,然后 为您的 ROS 2 安装提供源代码 以便 玫瑰2
命令即可运行。
导航进入 ros2_ws
目录中创建的 上一个教程.导航至 ros2_ws/src
目录,并创建一个新软件包:
ros2 pkg create --build-type ament_cmake bag_recorder_nodes --dependencies rclcpp rosbag2_cpp example_interfaces
您的终端将返回一条信息,验证是否创建了软件包 记录袋节点
及其所有必要的文件和文件夹。文件 --依赖
参数会自动在 package.xml
和 CMakeLists.txt
.在这种情况下,软件包将使用 rosbag2_cpp
软件包以及 rclcpp
软件包。对 接口示例
软件包也是本教程后续部分所必需的。
1.1 更新 package.xml
因为您使用了 --依赖
选项,就不必在创建软件包时手动将依赖关系添加到 package.xml
或 CMakeLists.txt
.不过,请务必一如既往地将说明、维护者电子邮件和姓名以及许可证信息添加到 package.xml
.
<描述>;C++ 背包 写作 教程</description>;
维护者 电子邮件="[email protected]";>;您的 名称维护人员</maintainer>;
许可证阿帕奇 许可证 2.0</license>;
2 编写 C++ 节点
内部 ros2_ws/src/bag_recorder_nodes/src
目录下,新建一个名为 simple_bag_recorder.cpp
并粘贴以下代码。
#include <rclcpp/rclcpp.hpp>;
#include <std_msgs/msg/string.hpp>;
#include <rosbag2_cpp/typesupport_helpers.hpp>;
#include <rosbag2_cpp/writer.hppt>;
#include <rosbag2_cpp/writers/sequential_writer.hpp>;
#include <rosbag2_storage/serialized_bag_message.hpp>;
使用 标准::占位符::_1;
类 简单包记录器 : 公 rclcpp::节点
{
公:
简单包记录器()
: 节点("simple_bag_recorder";)
{
缢 rosbag2_cpp::存储选项 存储选项({"my_bag";, "sqlite3";});
缢 rosbag2_cpp::转换器选项 转换器选项(
{rmw_get_serialization_format(),
rmw_get_serialization_format()});
作家_ = 标准::make_unique<;rosbag2_cpp::作家::顺序作家>;();
作家_->;开(存储选项, 转换器选项);
作家_->;创建主题(
{唠叨";,
"std_msgs/msg/String";,
rmw_get_serialization_format(),
"";});
订阅_ = 创建订阅<;std_msgs::信息::字符串>;(
唠叨";, 10, 标准::约束(及样品;简单包记录器::topic_callback, 此, _1));
}
私人:
空白 topic_callback(标准::共享_ptr<;rclcpp::序列化信息>; 信息) 缢
{
汽车 邮袋信息 = 标准::共享<;rosbag2_存储::序列化邮袋信息>;();
邮袋信息->;序列化数据 = 标准::共享_ptr<;rcutils_uint8_array_t>;(
新 rcutils_uint8_array_t,
[此](rcutils_uint8_array_t *信息) {
汽车 fini_return = rcutils_uint8_array_fini(信息);
删去 信息;
如果 (fini_return != RCUTILS_RET_OK) {
RCLCPP_ERROR(get_logger(),
"销毁序列化信息 %s" 失败;, rcutils_get_error_string().字符串);
}
});
*邮袋信息->;序列化数据 = 信息->;release_rcl_serialized_message();
邮袋信息->;topic_name = 唠叨";;
如果 (rcutils_system_time_now(及样品;邮袋信息->;时间戳) != RCUTILS_RET_OK) {
RCLCPP_ERROR(get_logger(), 获取当前时间出错:%s";,
rcutils_get_error_string().字符串);
}
作家_->;写道(邮袋信息);
}
rclcpp::订阅<;rclcpp::序列化信息>::SharedPtr 订阅_;
标准::唯一参数<;rosbag2_cpp::作家::顺序作家>; 作家_;
};
int 主要(int 参数, 烧焦 * 参数[])
{
rclcpp::启动(参数, 参数);
rclcpp::后旋(标准::共享<;简单包记录器>;());
rclcpp::关闭();
返回 0;
}
2.1 检查代码
"(《世界人权宣言》) #include
顶部的语句是软件包的依赖关系。注意包含了 rosbag2_cpp
包中提供了处理袋文件所需的函数和结构。
在类的构造函数中,我们首先要创建一个写入器对象,用于将数据写入数据包。我们必须提供袋子的存储选项。这些选项指定了名称 (我的包
)和格式(sqlite3
)的数据包。我们还必须提供转换选项,指定输入写入器的数据将如何序列化,以及写入袋时相同数据应如何序列化。在大多数情况下,您可以将这些选项指定为相同的值,因为无需转换序列化格式。我们使用 rmw_get_serialization_format()
函数来获取底层中间件使用的序列化格式。这是数据接收时的序列化格式,也是我们存储在数据包中的格式。
最后,第三行创建 writer 对象。我们创建一个 顺序作家
是最简单的写入器对象。它不执行高级操作,如在数据写入数据包时对数据进行压缩。
rosbag2_cpp::存储选项 存储选项({"my_bag";, "sqlite3";});
rosbag2_cpp::转换器选项 转换器选项({rmw_get_serialization_format(), rmw_get_serialization_format()});
作家_ = 标准::make_unique<;rosbag2_cpp::作家::顺序作家>;();
现在我们有了写入器对象,可以用它来打开袋子。
作家_->;开(存储选项, 转换器选项);
下一步是将我们要写入书包的每个主题通知作者。这可以通过调用 创建主题
的实例,并将 rosbag2_storage::TopicMetadata
结构。在这里,我们使用现代 C++ 语法就地构建了该结构的实例,而不是单独创建并传入。存储在 rosbag2_storage::TopicMetadata
结构是
主题名称。请注意,该名称不必与接收数据的主题相同。
主题中的数据类型。该类型 必须 与数据包中存储的数据类型相同。
数据的序列化格式。和以前一样,我们只需使用与底层中间件相同的序列化格式。
为主题指定的任何 QoS 设置。这些设置必须以 YAML 格式指定。留空字符串将使用系统默认值。
作家_->;创建主题({唠叨";, "std_msgs/msg/String";, rmw_get_serialization_format(), "";});
现在写入器已经设置好记录我们传递给它的数据,我们创建一个订阅并为其指定一个回调。我们将在回调中向袋子写入数据。
订阅_ = 创建订阅<;std_msgs::信息::字符串>;(
唠叨";, 10, 标准::约束(及样品;简单包记录器::topic_callback, 此, _1));
回调本身与典型的回调不同。我们接收的不是主题数据类型的实例,而是一个 rclcpp::SerializedMessage
.我们这样做有两个原因。
信息数据在写入数据包之前需要序列化,因此我们要求 ROS 将序列化后的信息原封不动地提供给我们,而不是在接收数据时将其取消序列化,然后再重新序列化。
写入器应用程序接口需要序列化信息,因此,通过请求 ROS 提供序列化信息,我们就省去了自己序列化数据的工作。
空白 topic_callback(标准::共享_ptr<;rclcpp::序列化信息>; 信息) 缢
{
在订阅回调中,首先要做的是创建一个 rosbag2_storage::SerializedBagMessage
.这是一种数据类型,用于表示数据袋中的单个数据样本。
汽车 邮袋信息 = 标准::共享<;rosbag2_存储::序列化邮袋信息>;();
由于直接处理来自中间件的序列化数据的内存管理要求,下一个代码块有些复杂。序列化数据的内存由 序列化信息
对象,但 序列化邮袋信息
对象必须拥有内存。否则,内存可能会超出作用域,并在写入 bag 之前被删除,从而导致内存访问错误。为了避免这种情况,我们调用 release_rcl_serialized_message()
在 序列化信息
对象。这将使其释放对内存的所有权,从而允许 序列化邮袋信息
掌握主动权。
然而,我们还需要确保 序列化邮袋信息
对象在清理时会正确删除内存。这可以通过在创建 序列化数据
成员 邮袋信息
实例。这就是将 lambda 函数传递到 std::shared_ptr<rcutils_uint8_array_t>;
对象(即 序列化数据
成员 邮袋信息
对象)。当 共享_ptr
退出作用域后,lambda 函数将被调用并清理内存。
邮袋信息->;序列化数据 = 标准::共享_ptr<;rcutils_uint8_array_t>;(
新 rcutils_uint8_array_t,
[此](rcutils_uint8_array_t *信息) {
汽车 fini_return = rcutils_uint8_array_fini(信息);
删去 信息;
如果 (fini_return != RCUTILS_RET_OK) {
RCLCPP_ERROR(get_logger(),
"销毁序列化信息 %s" 失败;, rcutils_get_error_string().字符串);
}
});
*邮袋信息->;序列化数据 = 信息->;release_rcl_serialized_message();
下一行用于告诉编写者此示例适用于哪个主题。这是必要的,因为序列化后的信息不包含类型信息,可能被错误地写入信息袋中的任何主题。
邮袋信息->;topic_name = 唠叨";;
信息的时间戳也必须在 时间戳
成员 邮袋信息
objecT。这个值可以是任何适合数据的值,但有两个常用值,一个是数据产生的时间(如果已知),另一个是数据接收的时间。这里使用的是第二个选项,即接收时间。
如果 (rcutils_system_time_now(及样品;邮袋信息->;时间戳) != RCUTILS_RET_OK) {
RCLCPP_ERROR(get_logger(), 获取当前时间出错:%s";,
rcutils_get_error_string().字符串);
}
回调的最后一步是将数据传递给写入器对象,以便将其写入数据包。
作家_->;写道(邮袋信息);
该类包含两个成员变量。
订阅对象。请注意,模板参数是回调的类型,而不是主题的类型。在这种情况下,回调会接收一个
rclcpp::SerializedMessage
共享指针,所以模板参数必须是这样的。用于写入数据包的写入器对象的受管指针。注意这里使用的写入器类型是
rosbag2_cpp::writers::SequentialWriter
.其他作家可能会有不同的行为。
rclcpp::订阅<;rclcpp::序列化信息>::SharedPtr 订阅_;
标准::唯一参数<;rosbag2_cpp::作家::顺序作家>; 作家_;
文件以 主要
函数用于创建节点实例并启动 ROS 处理。
int 主要(int 参数, 烧焦 * 参数[])
{
rclcpp::启动(参数, 参数);
rclcpp::后旋(标准::共享<;简单包记录器>;());
rclcpp::关闭();
返回 0;
}
2.2 添加可执行文件
现在打开 CMakeLists.txt
文件。在依赖关系下方 find_package(rosbag2_cpp 必填)
添加以下代码行。
add_executable(simple_bag_recorder src/simple_bag_recorder.cpp)
ament_target_dependencies(simple_bag_recorder rclcpp rosbag2_cpp)
install(TARGETS
简单记录袋
目的地 lib/${PROJECT_NAME}
)
3 构建和运行
返回工作区的根目录、 ros2_ws
然后创建新软件包。
colcon build --packages-select bag_recorder_nodes
colcon build --packages-select bag_recorder_nodes
colcon build --merge-install --packages-select bag_recorder_nodes
打开一个新终端,导航至 ros2_ws
并获取设置文件。
source install/setup.bash
source install/setup.bash
调用 install/setup.bat
为文件袋创建一个目录。该目录将包含构成一个包的所有文件。
mkdir my_bag
mkdir my_bag
mkdir my_bag
如果 我的包
目录已经存在,则必须先删除该目录,然后再重新创建)。
现在运行节点:
ros2 run bag_recorder_nodes simple_bag_recorder
打开第二个终端,运行 话匣子
示例节点。
ros2 run demo_nodes_cpp talker
这将开始在 唠叨
主题。写袋节点收到这些数据后,会将其写入 我的包
包。
终止两个节点。然后,在一个终端启动 听众
示例节点。
ros2 运行 demo_nodes_cpp 监听器
在另一个终端,使用 玫瑰2 背包
播放节点录制的音袋。
ros2 背包扮演 my_bag
您将看到来自信息包的信息正在被 听众
节点。
4 记录节点的合成数据
任何数据都可以记录到数据包中,而不仅仅是通过主题接收到的数据。从自己的节点写入数据包的一个常见用例是生成和存储合成数据。在本节中,您将学习如何编写一个节点,生成一些数据并将其存储到数据包中。我们将演示两种方法。第一种方法使用带有定时器的节点;如果数据生成是在节点外部进行的,例如直接从硬件(如摄像头)读取数据,则可以使用这种方法。第二种方法不使用节点;这是在不需要使用 ROS 基础架构的任何功能时可以使用的方法。
4.1 编写 C++ 节点
内部 ros2_ws/src/bag_recorder_nodes/src
目录下,新建一个名为 data_generator_node.cpp
并粘贴以下代码。
#include 时间顺序<chrono>;
#include <example_interfaces/msg/int32.hpp>;
#include <rclcpp/rclcpp.hpp>;
#include <rclcpp/serialization.hpp>;
#include <rosbag2_cpp/writer.hppt>;
#include <rosbag2_cpp/writers/sequential_writer.hpp>;
#include <rosbag2_storage/serialized_bag_message.hpp>;
使用 命名空间 标准::计时器;
类 数据生成器 : 公 rclcpp::节点
{
公:
数据生成器()
: 节点("data_generator";)
{
数据.数据 = 0;
缢 rosbag2_cpp::存储选项 存储选项({"timed_synthetic_bag";, "sqlite3";});
缢 rosbag2_cpp::转换器选项 转换器选项(
{rmw_get_serialization_format(),
rmw_get_serialization_format()});
作家_ = 标准::make_unique<;rosbag2_cpp::作家::顺序作家>;();
作家_->;开(存储选项, 转换器选项);
作家_->;创建主题(
{"合成";,
"example_interfaces/msg/Int32";,
rmw_get_serialization_format(),
"";});
定时器 = 创建隔离墙计时器(1s, 标准::约束(及样品;数据生成器::定时器回调, 此));
}
私人:
空白 定时器回调()
{
汽车 序列化器 = rclcpp::序列化<;接口示例::信息::Int32>;();
汽车 序列化信息 = rclcpp::序列化信息();
序列化器.序列化消息(及样品;数据, 及样品;序列化信息);
汽车 邮袋信息 = 标准::共享<;rosbag2_存储::序列化邮袋信息>;();
邮袋信息->;序列化数据 = 标准::共享_ptr<;rcutils_uint8_array_t>;(
新 rcutils_uint8_array_t,
[此](rcutils_uint8_array_t *信息) {
汽车 fini_return = rcutils_uint8_array_fini(信息);
删去 信息;
如果 (fini_return != RCUTILS_RET_OK) {
RCLCPP_ERROR(get_logger(),
"销毁序列化信息 %s" 失败;, rcutils_get_error_string().字符串);
}
});
*邮袋信息->;序列化数据 = 序列化信息.release_rcl_serialized_message();
邮袋信息->;topic_name = "合成";;
如果 (rcutils_system_time_now(及样品;邮袋信息->;时间戳) != RCUTILS_RET_OK) {
RCLCPP_ERROR(get_logger(), 获取当前时间出错:%s";,
rcutils_get_error_string().字符串);
}
作家_->;写道(邮袋信息);
++数据.数据;
}
rclcpp::定时器基数::SharedPtr 定时器;
标准::唯一参数<;rosbag2_cpp::作家::顺序作家>; 作家_;
接口示例::信息::Int32 数据;
};
int 主要(int 参数, 烧焦 * 参数[])
{
rclcpp::启动(参数, 参数);
rclcpp::后旋(标准::共享<;数据生成器>;());
rclcpp::关闭();
返回 0;
}
4.2 检查代码
该代码的大部分内容与第一个示例相同。重要的不同之处在此说明。
首先,改变袋子的名称。
rosbag2_cpp::存储选项 存储选项({"timed_synthetic_bag";, "sqlite3";});
将存储的主题名称和数据类型也不同,因此需要告诉编写者这一点。
作家_->;创建主题({"合成";, "example_interfaces/msg/Int32";, rmw_get_serialization_format(), "";});
该节点有一个计时器,而不是对主题的订阅。定时器以一秒为周期触发,并在触发时调用给定的成员函数。
定时器 = 创建隔离墙计时器(1s, 标准::约束(及样品;数据生成器::定时器回调, 此));
在定时器回调中,我们生成(或以其他方式获取,例如从连接到某些硬件的串行端口读取)我们希望存储在袋中的数据。这与上一个示例的重要区别在于,数据尚未序列化。因为数据包写入器期待的是序列化数据,所以我们必须先将其序列化。可以使用 rclcpp::Serialization
类。
汽车 序列化器 = rclcpp::序列化<;接口示例::信息::Int32>;();
汽车 序列化信息 = rclcpp::序列化信息();
序列化器.序列化消息(及样品;数据, 及样品;序列化信息);
回调代码的其余部分与此相同,只是稍作修改,以考虑主题名称和数据类型的差异,并在每次执行回调时递增数据。
4.3 添加可执行文件
打开 CMakeLists.txt
文件,并在之前添加的行之后添加以下行(特别是在 install(TARGETS ...)
宏调用)。
add_executable(data_generator_node src/data_generator_node.cpp)
ament_target_dependencies(data_generator_node rclcpp rosbag2_cpp example_interfaces)
install(TARGETS
数据生成器节点
目的地 lib/${PROJECT_NAME}
)
4.4 构建和运行
返回工作区的根目录、 ros2_ws
,并创建您的软件包。
colcon build --packages-select bag_recorder_nodes
colcon build --packages-select bag_recorder_nodes
colcon build --merge-install --packages-select bag_recorder_nodes
打开一个新终端,导航至 ros2_ws
并获取设置文件。
source install/setup.bash
source install/setup.bash
调用 install/setup.bat
为文件袋创建一个目录。该目录将包含构成一个包的所有文件。
mkdir timed_synthetic_bag
mkdir timed_synthetic_bag
mkdir timed_synthetic_bag
如果 定时合成袋
目录已经存在,则必须先删除该目录,然后再重新创建)。
现在运行节点:
ros2 run bag_recorder_nodes data_generator_node
等待 30 秒左右,然后使用 按住-c.接着,播放制作好的袋子。
ROS2合成袋定时播放
打开第二个终端,回显 /合成
主题。
ros2 topic echo /synthetic
您将看到以每秒一条信息的速度打印到控制台的已生成并存储在数据包中的数据。
5 记录可执行文件的合成数据
现在,您可以创建一个存储来自主题以外数据源的数据包,您将学习如何从非节点可执行文件生成和记录合成数据。这种方法的优点是代码简单,可以快速创建大量数据。
5.1 编写 C++ 可执行文件
内部 ros2_ws/src/bag_recorder_nodes/src
目录下,新建一个名为 data_generator_executable.cpp
并粘贴以下代码。
#include iostream>;
#include <rclcpp/rclcpp.hpp>;
#include <rclcpp/serialization.hpp>;
#include <example_interfaces/msg/int32.hpp>;
#include <rosbag2_cpp/writer.hppt>;
#include <rosbag2_cpp/writers/sequential_writer.hpp>;
#include <rosbag2_storage/serialized_bag_message.hpp>;
int 主要(int, 烧焦**)
{
接口示例::信息::Int32 数据;
数据.数据 = 0;
缢 rosbag2_cpp::存储选项 存储选项({"big_synthetic_bag";, "sqlite3";});
缢 rosbag2_cpp::转换器选项 转换器选项(
{rmw_get_serialization_format(),
rmw_get_serialization_format()});
标准::唯一参数<;rosbag2_cpp::作家::顺序作家>; 作家_ =
标准::make_unique<;rosbag2_cpp::作家::顺序作家>;();
作家_->;开(存储选项, 转换器选项);
作家_->;创建主题(
{"合成";,
"example_interfaces/msg/Int32";,
rmw_get_serialization_format(),
"";});
rcutils_time_point_value_t 时间戳;
如果 (rcutils_system_time_now(及样品;时间戳) != RCUTILS_RET_OK) {
标准::cerr <<; 获取当前时间出错:"; <<;
rcutils_get_error_string().字符串;
返回 1;
}
对于 (int32_t ii = 0; ii <; 100; ++ii) {
汽车 序列化器 = rclcpp::序列化<;接口示例::信息::Int32>;();
汽车 序列化信息 = rclcpp::序列化信息();
序列化器.序列化消息(及样品;数据, 及样品;序列化信息);
汽车 邮袋信息 = 标准::共享<;rosbag2_存储::序列化邮袋信息>;();
邮袋信息->;序列化数据 = 标准::共享_ptr<;rcutils_uint8_array_t>;(
新 rcutils_uint8_array_t,
[](rcutils_uint8_array_t *信息) {
汽车 fini_return = rcutils_uint8_array_fini(信息);
删去 信息;
如果 (fini_return != RCUTILS_RET_OK) {
标准::cerr <<; "销毁序列化信息失败"; <<;
rcutils_get_error_string().字符串;
}
});
*邮袋信息->;序列化数据 = 序列化信息.release_rcl_serialized_message();
邮袋信息->;topic_name = "合成";;
邮袋信息->;时间戳 = 时间戳;
作家_->;写道(邮袋信息);
++数据.数据;
时间戳 += 1000000000;
}
返回 0;
}
5.2 检查代码
比较一下这个示例和上一个示例就会发现,它们并没有什么不同。唯一明显的不同是使用 for 循环而不是定时器来驱动数据生成。
请注意,我们现在也在为数据生成时间戳,而不是依赖每个样本的当前系统时间。时间戳可以是你需要的任何值。数据将以这些时间戳给出的速度回放,因此这是控制样本默认回放速度的有效方法。还要注意的是,虽然每个采样之间的时间间隔是一整秒,但该可执行文件不需要在每个采样之间等待一秒钟。这样,我们就能在比回放时间更短的时间内生成大量数据,覆盖更宽的时间跨度。
rcutils_time_point_value_t 时间戳;
如果 (rcutils_system_time_now(及样品;时间戳) != RCUTILS_RET_OK) {
标准::cerr <<; 获取当前时间出错:"; <<;
rcutils_get_error_string().字符串;
返回 1;
}
对于 (int32_t ii = 0; ii <; 100; ++ii) {
...
邮袋信息->;时间戳 = 时间戳;
...
时间戳 += 1000000000;
}
5.3 添加可执行文件
打开 CMakeLists.txt
文件,并在之前添加的行之后添加以下行。
add_executable(data_generator_executable src/data_generator_executable.cpp)
ament_target_dependencies(data_generator_executable rclcpp rosbag2_cpp example_interfaces)
install(TARGETS
数据生成器可执行
目的地 lib/${PROJECT_NAME}
)
5.4 构建和运行
返回工作区的根目录、 ros2_ws
,并创建您的软件包。
colcon build --packages-select bag_recorder_nodes
colcon build --packages-select bag_recorder_nodes
colcon build --merge-install --packages-select bag_recorder_nodes
打开终端,导航至 ros2_ws
并获取设置文件。
source install/setup.bash
source install/setup.bash
调用 install/setup.bat
为文件袋创建一个目录。该目录将包含构成一个包的所有文件。
mkdir big_synthetic_bag
mkdir big_synthetic_bag
mkdir big_synthetic_bag
如果 大合成包
目录已经存在,则必须先删除该目录,然后再重新创建)。
现在运行可执行文件:
ros2 run bag_recorder_nodes data_generator_executable
请注意,可执行文件的运行和结束速度都非常快。
现在回放制作好的袋子。
ROS2 袋装大合成树脂袋
打开第二个终端,回显 /合成
主题。
ros2 topic echo /synthetic
您将看到以每秒一条信息的速度将生成并存储在数据袋中的数据打印到控制台。尽管数据包的生成速度很快,但仍会按照时间戳指示的速度进行回放。
摘要
您创建了一个节点,它可以将收到的主题数据记录到数据包中。您测试了使用该节点记录数据包,并通过回放数据包来验证数据是否被记录。然后,您继续创建了一个节点和一个可执行文件,用于生成合成数据并将其存储到数据包中。