260418

接下来就要让我们动态位置的旗帜和我们的小车这两者进行对接了。

至于为什么要对接嘛...因为我们的 /flag_pose 是使用的Gazebo 世界位姿真值,而我们小车控制还是使用的 /odom,如果这个 /odom 是里程计积分量,不是世界真值,那目标一旦多次挪动,两个坐标系就会越来越对不上。

也就会出现在我们移动了旗帜后,第一次如果角度不大的话,往往小车可以正常过去,但是第二次和多几次后,小车就会往看似和旗帜方向无关的方向行驶一段距离后就停下了,不会按照预期来跟随旗帜。

我们先给 src/kibot_one_sim/models/kibot_one_base/model.sdf 加入和我们旗帜相同的插件,发布小车自己的位置:

    <plugin
      filename="gz-sim-pose-publisher-system"
      name="gz::sim::systems::PosePublisher">
      <publish_model_pose>true</publish_model_pose>
      <publish_link_pose>false</publish_link_pose>
      <use_pose_vector_msg>false</use_pose_vector_msg>
      <static_publisher>false</static_publisher>
      <update_frequency>20</update_frequency>
    </plugin>

然后,同样的,在 src/kibot_one_sim/config/ros_gz_bridge.yaml 中加入这个桥接的话题:

- ros_topic_name: "/robot_pose"
  gz_topic_name: "/model/kibot_one_base/pose"
  ros_type_name: "geometry_msgs/msg/PoseStamped"
  gz_type_name: "gz.msgs.Pose"
  direction: GZ_TO_ROS
  frame_id: "world"

在 src/kibot_one_control/kibot_one_control/follow_controller.py 下加入我们的这个参数、变量和订阅者:

class FollowController(Node):
    def __init__(self) -> None:
        # ...
        params = [
            # ...
            ('robot_pose_topic', 'robot_pose'),
            # ...
        ]
        # ...
        self.has_robot_pose = False
        # ...
        self.create_subscription(
            msg_type=PoseStamped,
            topic=cast(str, self.get_parameter('robot_pose_topic').value),
            callback=self._robot_pose_callback,
            qos_profile=10,
        )
        # ...

这里我们引入了 self.has_robot_pose 这个变量,用途是为了作为开关,来避免两个不同来源同时写 robot_x / robot_y / robot_yaw。

因为我们当前的 follow_controller 有两个输入源,分别是 /odom和 /robot_pose,它们都会更新我们提到的上述三个状态。

因此,如果不加 self.has_robot_pose 这个变量,则就会出现输入源混乱的情况。

紧接着,修改我们的 _odom_callback 这个旧的 odom 回调:

    def _odom_callback(self, msg: Odometry) -> None:
        if self.has_robot_pose:
            return

        orientation = msg.pose.pose.orientation
        self.robot_x = msg.pose.pose.position.x
        self.robot_y = msg.pose.pose.position.y
        siny_cosp = 2.0 * (orientation.w * orientation.z + orientation.x * orientation.y)
        cosy_cosp = 1.0 - 2.0 * (orientation.y * orientation.y + orientation.z * orientation.z)
        self.robot_yaw = math.atan2(siny_cosp, cosy_cosp)

然后新增 _robot_pose_callback 这个回调,作为我们 robot_pose_topic 这个话题的回调:

    def _robot_pose_callback(self, msg: PoseStamped) -> None:
        self.has_robot_pose = True
        self.robot_x = msg.pose.position.x
        self.robot_y = msg.pose.position.y

        orientation = msg.pose.orientation
        siny_cosp = 2.0 * (orientation.w * orientation.z + orientation.x * orientation.y)
        cosy_cosp = 1.0 - 2.0 * (orientation.y * orientation.y + orientation.z * orientation.z)
        self.robot_yaw = math.atan2(siny_cosp, cosy_cosp)

我们会发现我们的这一段代码出现了两次:

        siny_cosp = 2.0 * (orientation.w * orientation.z + orientation.x * orientation.y)
        cosy_cosp = 1.0 - 2.0 * (orientation.y * orientation.y + orientation.z * orientation.z)
        self.robot_yaw = math.atan2(siny_cosp, cosy_cosp)

为了确保一致性,我们可以将这个代码整理为一个辅助函数:

    @staticmethod
    def _yaw_from_quaternion(orientation) -> float:
        siny_cosp = 2.0 * (orientation.w * orientation.z + orientation.x * orientation.y)
        cosy_cosp = 1.0 - 2.0 * (orientation.y * orientation.y + orientation.z * orientation.z)
        return math.atan2(siny_cosp, cosy_cosp)

现在我们的这两个回调就长这样了:

    def _odom_callback(self, msg: Odometry) -> None:
        if self.has_robot_pose:
            return

        self.robot_x = msg.pose.pose.position.x
        self.robot_y = msg.pose.pose.position.y
        self.robot_yaw = self._yaw_from_quaternion(msg.pose.pose.orientation)

    def _robot_pose_callback(self, msg: PoseStamped) -> None:
        self.has_robot_pose = True
        self.robot_x = msg.pose.position.x
        self.robot_y = msg.pose.position.y
        self.robot_yaw = self._yaw_from_quaternion(msg.pose.orientation)

相对来说简化了一点。

接下来就是构建,然后启动看看效果了:

可以看到效果完全符合我们的预期。

就这样,我们修复了坐标系对齐的问题。

以上。