25 实践课-Na 导航入门与导航控制节点开发

Nav2 导航入门与导航控制节点开发

关联:索引

0)快速自检:先确认接口依赖是否存在(证据优先)

source /opt/ros/humble/setup.bash
ros2 pkg list | grep -E "nav2_msgs|geometry_msgs|action_msgs" | head

逐行解释:

如果查不到(常见于未安装),按需安装(示例,择一即可,以你机器实际可安装包为准):

sudo apt update
sudo apt install -y ros-humble-navigation2

逐行解释:

本当前采用:路径 B(Fake NavigateToPose)。

迁移原则(从 B 到 A):


导航任务 = Action(Goal → Feedback(多次) → Result) + Cancel

三条“零基础必背”:

  1. 导航是长任务,所以必须能看到过程反馈(feedback),并且必须允许取消(cancel)。
  2. 你的“导航控制节点”本质是一个动作客户端:订阅 goal → 发 action goal → 读反馈 → 等结果 → 提供取消入口 → 输出证据状态。

1)启动 Fake 动作服务器(必须完成)

本节目标:在完全没有 AGV/仿真的情况下,仍然让学生具备“动作客户端开发”的前置证据——动作存在、可反馈、可取消、可返回结果。

创建 Fake 动作服务器包(Python):

source /opt/ros/humble/setup.bash
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
ros2 pkg create sorting_nav2_fake --build-type ament_python --dependencies rclpy nav2_msgs geometry_msgs action_msgs

建议文件路径:

入口点配置(setup.py):

entry_points={
    'console_scripts': [
        'fake_navigate_to_pose_server = sorting_nav2_fake.fake_navigate_to_pose_server:main',
    ],
},

Fake 动作服务器代码(可直接复制):

import time  # 用于模拟“导航需要时间”,并控制 feedback 的发布频率

import rclpy  # ROS 2 Python 客户端库(rclpy)
from rclpy.action import ActionServer  # 动作服务端(接收 goal / 发送 feedback / 返回 result)
from rclpy.action import CancelResponse  # 取消请求的应答类型(接受/拒绝)
from rclpy.action import GoalResponse  # goal 请求的应答类型(接受/拒绝)
from rclpy.node import Node  # ROS 2 节点基类

from nav2_msgs.action import NavigateToPose  # Nav2 的 NavigateToPose 动作接口定义

class FakeNavigateToPoseServer(Node):
    """
    Fake NavigateToPose 动作服务器(课堂用,无底盘/无仿真也可运行)

    作用:
    - 提供与 Nav2 同名同类型的动作接口:/navigate_to_pose (nav2_msgs/action/NavigateToPose)
    - 让学生在“没有机器人运动”的条件下,仍能练习 Action 的完整交互:
      goal → feedback(多次) → result,以及 cancel

    注意:
    - 这不是导航算法,也不会让机器人动起来。
    - 这里只模拟“距离逐步减少”,便于产生可观察的 feedback。
    """

    def __init__(self):
        super().__init__('fake_navigate_to_pose_server')
        # ActionServer 的第三个参数是 action 名称。
        # 真实 Nav2 中通常为 'navigate_to_pose'(对应 /navigate_to_pose)。
        self._server = ActionServer(
            self,
            NavigateToPose,
            'navigate_to_pose',
            execute_callback=self.execute_callback,
            goal_callback=self.goal_callback,
            cancel_callback=self.cancel_callback,
        )

    def goal_callback(self, goal_request: NavigateToPose.Goal) -> GoalResponse:
        # goal_callback:在执行前做一次“是否接受目标”的快速判断。
        # 真实系统里可做更多校验:frame_id 合法性、坐标范围、速度限制等。
        frame_id = getattr(getattr(goal_request, "pose", None), "header", None).frame_id
        if not frame_id:
            # 没有 frame_id 直接拒绝,帮助学生理解“接口契约”与“证据化失败”。
            return GoalResponse.REJECT
        return GoalResponse.ACCEPT

    def cancel_callback(self, goal_handle) -> CancelResponse:
        # cancel_callback:收到取消请求时,决定是否接受取消。
        # 课堂直接接受,确保 cancel 流程一定可演示。
        return CancelResponse.ACCEPT

    def execute_callback(self, goal_handle):
        # execute_callback:真正执行“导航任务”的逻辑。
        # 这里用 while 循环模拟“还在走”,每 0.1 秒发布一次 feedback。
        feedback = NavigateToPose.Feedback()
        distance = 2.0
        while distance > 0.0:
            if goal_handle.is_cancel_requested:
                # 一旦检测到取消请求:
                # 1) 标记为 canceled;2) 返回一个 Result(这里填入可读的 error_msg)
                goal_handle.canceled()
                result = NavigateToPose.Result()
                result.error_code = 0
                result.error_msg = "canceled (fake)"
                return result

            # “距离逐步减少”只是为了让 feedback 有变化,方便验收截图/日志对比。
            distance = max(0.0, distance - 0.1)
            feedback.distance_remaining = float(distance)
            feedback.number_of_recoveries = 0
            goal_handle.publish_feedback(feedback)
            # 课堂写法:time.sleep 会阻塞当前执行线程,用于最小可复现。
            # 生产环境通常用更可控的异步/定时器模型,避免长时间阻塞执行器线程。
            time.sleep(0.1)

        # 任务正常结束:标记 succeed 并返回 Result
        goal_handle.succeed()
        result = NavigateToPose.Result()
        result.error_code = 0
        result.error_msg = "succeeded (fake)"
        return result

def main(args=None):
    # rclpy.init:初始化 ROS 2 通信(必须先 init 才能创建 Node)
    rclpy.init(args=args)
    node = FakeNavigateToPoseServer()
    try:
        # spin:进入事件循环,处理 action goal、cancel 等回调
        rclpy.spin(node)
    finally:
        # 退出时释放资源
        node.destroy_node()
        rclpy.shutdown()

if __name__ == "__main__":
    main()

编译并启动 Fake 动作服务器:

cd ~/ros2_ws
colcon build --symlink-install
source /opt/ros/humble/setup.bash
source install/setup.bash
ros2 run sorting_nav2_fake fake_navigate_to_pose_server

动作存在证据(带类型):

ros2 action list -t | grep -E "navigate_to_pose|NavigateToPose"

用命令行直接发一次目标(得到 feedback 与 result 证据):

ros2 action send_goal /navigate_to_pose nav2_msgs/action/NavigateToPose "{
  pose: {header: {frame_id: 'map'}, pose: {position: {x: 1.0, y: 0.5, z: 0.0}, orientation: {w: 1.0}}}
}" --feedback

取消证据(取消当前动作目标):

ros2 action cancel /navigate_to_pose
  1. 看不到 navigate_to_pose:先确认 Fake server 已运行,再执行 source install/setup.bash,最后 ros2 action list -t 查证据。
  2. send_goal 没有反馈:先确认 ros2 action list -t 能看到类型为 nav2_msgs/action/NavigateToPose,再检查 action 名称是否写成了 /navigate_to_pose
  3. cancel 没效果:先确认你取消的是同一个 action 名称(ros2 action cancel /navigate_to_pose),并观察服务端是否输出 canceled 日志。
ros2 interface show nav2_msgs/action/NavigateToPose

要求:你只能按 ros2 interface show 看到的字段写代码与排错,不能“脑补字段”。

1)创建导航控制包(Python)

source /opt/ros/humble/setup.bash
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
ros2 pkg create sorting_agv_nav --build-type ament_python --dependencies rclpy nav2_msgs geometry_msgs action_msgs std_msgs std_srvs

建议文件路径:

入口点配置(~/ros2_ws/src/sorting_agv_nav/setup.py):

entry_points={
    'console_scripts': [
        'agv_nav_controller = sorting_agv_nav.agv_nav_controller:main',
    ],
},
import rclpy  # ROS 2 Python 客户端库
from rclpy.action import ActionClient  # 动作客户端:发送 goal、接收 feedback、获取 result、发送 cancel
from rclpy.node import Node  # ROS 2 节点基类

from action_msgs.msg import GoalStatus  # Action 通用的状态枚举(SUCCEEDED / ABORTED / CANCELED ...)
from geometry_msgs.msg import PoseStamped  # 目标点类型:必须带 frame_id 的位姿
from nav2_msgs.action import NavigateToPose  # Nav2 NavigateToPose 动作定义(课堂对接 Fake server/真实 Nav2 均可)
from std_msgs.msg import String  # 课堂用 String 承载状态证据(工程建议后续换自定义 msg)
from std_srvs.srv import Trigger  # 最小取消入口:ros2 service call 即可触发

class AgvNavController(Node):
    """
    导航控制节点(动作客户端)

    设计目标(课堂最小闭环版):
    - 上层输入:订阅 PoseStamped(goal_topic)
    - 内部执行:调用 NavigateToPose Action(action_name)
    - 证据输出:发布 /sorting/agv/state(state_topic),包含 request_id/phase/step/msg
    - 最小取消:提供 Trigger 服务(cancel_service)

    迁移到真实 Nav2:
    - 保持本节点逻辑不变
    - 只需确保 action_name/topic/service 名称与真实系统一致
    """

    def __init__(self):
        super().__init__('agv_nav_controller')
        # 参数化:不要把 action/topic/service 名称写死在代码里,便于换环境复用。
        # 课堂默认:对接 Fake server 的 navigate_to_pose。
        self.declare_parameter("action_name", "navigate_to_pose")
        self.declare_parameter("goal_topic", "/sorting/agv/goal")
        self.declare_parameter("state_topic", "/sorting/agv/state")
        self.declare_parameter("cancel_service", "/sorting/agv/cancel")
        # wait_for_server_timeout_sec:等待动作服务端就绪的超时(避免无限等待)
        self.declare_parameter("wait_server_timeout_sec", 2.0)
        # qos_depth:简单起见统一用深度队列;真实系统应按数据类型选择 QoS
        self.declare_parameter("qos_depth", 10)

        # 读取参数值并缓存成成员变量,后续使用更清晰。
        self._action_name = str(self.get_parameter("action_name").value)
        self._goal_topic = str(self.get_parameter("goal_topic").value)
        self._state_topic = str(self.get_parameter("state_topic").value)
        self._cancel_service = str(self.get_parameter("cancel_service").value)
        self._wait_server_timeout_sec = float(self.get_parameter("wait_server_timeout_sec").value)
        self._qos_depth = int(self.get_parameter("qos_depth").value)

        # ActionClient:连接到动作服务端(这里是 Fake server 或真实 Nav2)
        self._client = ActionClient(self, NavigateToPose, self._action_name)
        # 订阅目标点:收到目标点才启动一次动作任务(避免节点启动就立刻发 goal)
        self._goal_sub = self.create_subscription(PoseStamped, self._goal_topic, self.on_goal, self._qos_depth)
        # 取消服务:通过 ros2 service call 触发取消,方便课堂给证据
        self._cancel_srv = self.create_service(Trigger, self._cancel_service, self.on_cancel)
        # 状态发布:统一的证据通道(终端 echo 即可验收)
        self._state_pub = self.create_publisher(String, self._state_topic, self._qos_depth)

        # 证据字段:
        # - seq:本节点内部递增序号,便于截图时定位“第几条状态”
        # - request_id:每次收到新目标就 +1,贯穿一次任务全过程
        self._seq = 0
        self._request_id = 0
        self._active_request_id = None
        # 记录最近一次 goal_handle,用于取消
        self._last_goal_handle = None
        # busy:课堂做一个简单串行执行(一次只跑一个目标),避免并发状态混乱
        self._busy = False

    def now_sec(self) -> float:
        # 使用 ROS Clock(兼容 use_sim_time 场景;课堂可当作“统一时间戳”)
        return self.get_clock().now().nanoseconds / 1e9

    def status_text(self, status: int) -> str:
        # 把 GoalStatus 的数字状态码映射成可读字符串,便于学生截图/复盘。
        if status == GoalStatus.STATUS_UNKNOWN:
            return "UNKNOWN"
        if status == GoalStatus.STATUS_ACCEPTED:
            return "ACCEPTED"
        if status == GoalStatus.STATUS_EXECUTING:
            return "EXECUTING"
        if status == GoalStatus.STATUS_CANCELING:
            return "CANCELING"
        if status == GoalStatus.STATUS_SUCCEEDED:
            return "SUCCEEDED"
        if status == GoalStatus.STATUS_CANCELED:
            return "CANCELED"
        if status == GoalStatus.STATUS_ABORTED:
            return "ABORTED"
        return str(status)

    def publish_state(self, phase: str, step: str, message: str, request_id: int | None = None):
        # phase:跨模块统一口径(idle/executing/success/failed/canceled)
        # step:模块内细分阶段(received_goal/wait_server/send_goal/feedback/result/cancel ...)
        # message:给“证据可复盘”的信息(比如错误原因、目标点、状态码)
        self._seq += 1
        rid = request_id if request_id is not None else self._active_request_id
        msg = String()
        msg.data = f"seq={self._seq} request_id={rid} ros_ts={self.now_sec():.3f} phase={phase} step={step} msg={message}"
        self._state_pub.publish(msg)
        # 同时打印日志:一份证据在 topic 和日志中都能看到
        self.get_logger().info(msg.data)

    def build_goal(self, pose: PoseStamped):
        # 把输入 PoseStamped 转成 NavigateToPose.Goal
        # 这里重新写 stamp,避免上游给的时间戳过旧导致服务端拒绝/异常(真实系统常见坑)。
        goal = NavigateToPose.Goal()
        p = PoseStamped()
        p.header.frame_id = pose.header.frame_id
        p.header.stamp = self.get_clock().now().to_msg()
        p.pose = pose.pose
        goal.pose = p
        return goal

    def on_goal(self, pose_msg: PoseStamped):
        # 订阅回调:收到新目标点就启动一次动作调用。
        if self._busy:
            # 课堂简化:忙时直接拒绝新目标(避免并发)
            self.publish_state("executing", "busy", "ignored new goal because controller is busy")
            return

        # 新目标:生成新的 request_id,并作为“贯穿证据”的主键
        self._request_id += 1
        self._active_request_id = self._request_id
        self.publish_state(
            "executing",
            "received_goal",
            f"frame={pose_msg.header.frame_id} x={pose_msg.pose.position.x:.3f} y={pose_msg.pose.position.y:.3f}",
            request_id=self._active_request_id,
        )

        # 等待动作服务端就绪(Fake server 或真实 Nav2);设置超时,避免课堂“卡死”
        self.publish_state("executing", "wait_server", f"waiting for {self._action_name} action server")
        if not self._client.wait_for_server(timeout_sec=self._wait_server_timeout_sec):
            self.publish_state("failed", "wait_server", "action server not available")
            self._active_request_id = None
            return

        # 构造 goal 并异步发送
        goal = self.build_goal(pose_msg)
        self._busy = True
        self.publish_state(
            "executing",
            "send_goal",
            f"frame={pose_msg.header.frame_id} x={pose_msg.pose.position.x:.3f} y={pose_msg.pose.position.y:.3f}",
            request_id=self._active_request_id,
        )
        # send_goal_async:异步发送 goal,避免阻塞执行器线程
        # feedback_callback:服务端每发一次 feedback,就触发 on_feedback
        future = self._client.send_goal_async(goal, feedback_callback=self.on_feedback)
        # future 完成后会得到 goal_handle(accepted/rejected)
        future.add_done_callback(self.on_goal_response)

    def on_goal_response(self, future):
        # 处理 send_goal_async 的返回(goal_handle)
        try:
            goal_handle = future.result()
        except Exception as e:
            self._busy = False
            self.publish_state("failed", "send_goal", f"send_goal failed: {type(e).__name__}: {e}")
            self._active_request_id = None
            return

        if not goal_handle.accepted:
            # 目标被拒绝:常见原因包括 frame_id 为空、参数不合法等
            self._busy = False
            self.publish_state("failed", "goal_response", "goal rejected")
            self._active_request_id = None
            return

        # 目标被接受:保存 goal_handle,用于后续 cancel
        self._last_goal_handle = goal_handle
        self.publish_state("executing", "goal_response", "goal accepted")

        # get_result_async:异步等待最终结果(SUCCEEDED/ABORTED/CANCELED)
        result_future = goal_handle.get_result_async()
        result_future.add_done_callback(self.on_result)

    def on_result(self, future):
        # 处理最终结果(Result + status)
        self._busy = False
        try:
            result = future.result()
        except Exception as e:
            self.publish_state("failed", "result", f"get_result failed: {type(e).__name__}: {e}")
            self._active_request_id = None
            self._last_goal_handle = None
            return

        # result.status:GoalStatus 状态码(不是 NavigateToPose.Result 的字段)
        status = int(getattr(result, "status", 0))
        status_text = self.status_text(status)

        # result.result:NavigateToPose.Result(含 error_code / error_msg)
        nav_result = getattr(result, "result", None)
        error_code = getattr(nav_result, "error_code", None) if nav_result is not None else None
        error_msg = getattr(nav_result, "error_msg", None) if nav_result is not None else None

        # 把动作层状态码映射为课堂统一 phase
        if status == GoalStatus.STATUS_SUCCEEDED:
            phase = "success"
        elif status == GoalStatus.STATUS_CANCELED:
            phase = "canceled"
        else:
            phase = "failed"

        # 拼装可读结果文本,作为证据输出
        parts = [f"status={status_text}"]
        if error_code is not None:
            parts.append(f"error_code={error_code}")
        if error_msg:
            parts.append(f"error_msg={error_msg}")
        self.publish_state(phase, "result", " ".join(parts))

        # 清理一次任务的上下文
        self._active_request_id = None
        self._last_goal_handle = None

    def on_feedback(self, msg):
        # feedback 回调:尽量输出关键字段,避免日志过长难截图
        fb = msg.feedback
        parts = []
        distance_remaining = getattr(fb, "distance_remaining", None)
        if distance_remaining is not None:
            parts.append(f"distance_remaining={float(distance_remaining):.3f}")
        recoveries = getattr(fb, "number_of_recoveries", None)
        if recoveries is not None:
            parts.append(f"recoveries={int(recoveries)}")
        if not parts:
            parts.append(str(fb))
        self.publish_state("executing", "feedback", " ".join(parts))

    def on_cancel(self, request, response):
        # Trigger 服务回调:把“取消”做成可 CLI 触发、可给证据的入口
        if self._last_goal_handle is None:
            response.success = False
            response.message = "no active goal"
            self.publish_state("idle", "cancel", "no active goal to cancel")
            return response

        self.publish_state("executing", "cancel", "cancel requested")
        # cancel_goal_async:向服务端发送取消请求(异步)
        cancel_future = self._last_goal_handle.cancel_goal_async()
        cancel_future.add_done_callback(self.on_cancel_done)
        response.success = True
        response.message = "cancel sent"
        return response

    def on_cancel_done(self, future):
        # 取消请求的结果回调:返回 cancel_response(含 return_code)
        try:
            cancel_response = future.result()
        except Exception as e:
            self.publish_state("failed", "cancel", f"cancel failed: {type(e).__name__}: {e}")
            return

        self.publish_state("executing", "cancel", f"return_code={cancel_response.return_code}")

def main(args=None):
    # 入口函数:初始化 → 创建节点 → spin → 退出清理
    rclpy.init(args=args)
    node = AgvNavController()

    try:
        rclpy.spin(node)
    finally:
        node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

3)参数文件示例(推荐)

agv_nav_controller:
  ros__parameters:
    action_name: navigate_to_pose
    goal_topic: /sorting/agv/goal
    state_topic: /sorting/agv/state
    cancel_service: /sorting/agv/cancel
    wait_server_timeout_sec: 2.0
    qos_depth: 10

终端 A(先启动 Fake 动作服务器,提供 navigate_to_pose):

cd ~/ros2_ws
colcon build --symlink-install
source /opt/ros/humble/setup.bash
source install/setup.bash
ros2 run sorting_nav2_fake fake_navigate_to_pose_server

编译与运行:

cd ~/ros2_ws
colcon build --symlink-install
source /opt/ros/humble/setup.bash
source install/setup.bash
ros2 run sorting_agv_nav agv_nav_controller --ros-args --params-file /path/to/agv_nav_params.yaml

证据 1:状态通道可见

ros2 topic echo /sorting/agv/state

证据 2:下发目标(与参数文件保持一致)

ros2 topic pub --once /sorting/agv/goal geometry_msgs/msg/PoseStamped "{
  header: {frame_id: 'map'},
  pose: {
    position: {x: 1.0, y: 0.5, z: 0.0},
    orientation: {x: 0.0, y: 0.0, z: 0.0, w: 1.0}
  }
}"

证据 3:取消请求

ros2 service call /sorting/agv/cancel std_srvs/srv/Trigger {}

作业(布置)

1)提交动作就绪证据(截图或终端输出均可):ros2 action list -t(能看到 navigate_to_pose,类型为 nav2_msgs/action/NavigateToPose)。

2)提交 AGV 导航控制节点完整代码(订阅目标、调用 NavigateToPose、输出 state、支持 cancel),并附 150 字说明:你的 request_id/phase/step 设计如何保证可追溯。

3)提交控制节点测试证据:至少包含 1 次目标下发与 1 次取消(服务调用)终端输出,且能在 /sorting/agv/state 中看到过程反馈与最终结果。

4)提交一次失败排查记录(200–300 字):按“现象→证据→定位→修复→复测”结构,问题可选“动作 server 不可用/目标被拒绝/反馈无输出/取消无效/依赖缺失”等任意一种,必须有证据输出。


说明:

迁移指南:从路径 B 接入真实 Nav2 + 地图(路径 A)

你可以按“先接入真实动作接口 → 再接入地图/定位/底盘”的顺序迁移,风险最低:

  1. 保留 sorting_agv_navagv_nav_controller 不变,先停止 Fake server。
  2. 启动真实 Nav2(以及底盘/仿真),用 ros2 action list -t 确认真实系统存在 navigate_to_pose(类型为 nav2_msgs/action/NavigateToPose)。
  3. 修改 agv_nav_params.yaml(或运行时参数)把 action_name 对齐真实系统动作名(多数情况下仍为 navigate_to_pose,但以 ros2 action list -t 证据为准)。
  4. goal_topic/state_topic/cancel_service 保持不变,确保上层接口不变,联调成本最低。
  5. 引入地图:把静态地图(classroom_map.yaml + classroom_map.pgm 或你的真实地图)放到可访问路径,并在 Nav2 bringup 时传入 map:=/path/to/map.yaml
  6. 确认定位与 TF:至少要有 map→odom→base_link 链路;若用 AMCL,需要在 RViz2 里设置初始位姿;若用 SLAM,启动方式与参数不同。
  7. 用 RViz2 先点一次 2D Nav Goal 验证真实导航可跑,再用 agv_nav_controller/sorting/agv/goal 验证“控制节点→真实 Nav2”闭环。

在你的工作空间创建地图文件(复制粘贴即可):

mkdir -p ~/ros2_ws/maps
cd ~/ros2_ws/maps

1)地图 YAML:~/ros2_ws/maps/classroom_map.yaml

image: classroom_map.pgm
mode: trinary
resolution: 0.05
origin: [0.0, 0.0, 0.0]
negate: 0
occupied_thresh: 0.65
free_thresh: 0.196

2)地图 PGM:~/ros2_ws/maps/classroom_map.pgm

P2
# classroom_map.pgm (20x20) 0=occupied, 254=free
20 20
255
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 254 254 254 254 254 254 254 254 254 254 254 254 254 254 254 254 254 254 0
0 254 254 254 254 254 254 254 254 254 254 254 254 254 254 254 254 254 254 0
0 254 254 0 0 0 254 254 254 254 254 254 254 0 0 0 254 254 254 0
0 254 254 0 254 0 254 254 254 254 254 254 254 0 254 0 254 254 254 0
0 254 254 0 0 0 254 254 254 254 254 254 254 0 0 0 254 254 254 0
0 254 254 254 254 254 254 0 0 0 0 0 254 254 254 254 254 254 254 0
0 254 254 254 254 254 254 0 254 254 254 0 254 254 254 254 254 254 254 0
0 254 254 254 254 254 254 0 254 254 254 0 254 254 254 254 254 254 254 0
0 254 254 254 254 254 254 0 0 0 0 0 254 254 254 254 254 254 254 0
0 254 254 254 254 254 254 254 254 254 254 254 254 254 254 254 254 254 254 0
0 254 254 254 0 0 0 254 254 254 254 254 254 0 0 0 254 254 254 0
0 254 254 254 0 254 0 254 254 254 254 254 254 0 254 0 254 254 254 0
0 254 254 254 0 0 0 254 254 254 254 254 254 0 0 0 254 254 254 0
0 254 254 254 254 254 254 254 254 0 0 0 254 254 254 254 254 254 254 0
0 254 254 254 254 254 254 254 254 0 254 0 254 254 254 254 254 254 254 0
0 254 254 254 254 254 254 254 254 0 0 0 254 254 254 254 254 254 254 0
0 254 254 254 254 254 254 254 254 254 254 254 254 254 254 254 254 254 254 0
0 254 254 254 254 254 254 254 254 254 254 254 254 254 254 254 254 254 254 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

提示: