<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-CN"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://1q08.github.io/ros_ws/feed.xml" rel="self" type="application/atom+xml" /><link href="https://1q08.github.io/ros_ws/" rel="alternate" type="text/html" hreflang="zh-CN" /><updated>2026-09-22T08:02:12+00:00</updated><id>https://1q08.github.io/ros_ws/feed.xml</id><title type="html">ROS 命令速查</title><subtitle>开源的 ROS 1 / ROS 2 命令查找工具</subtitle><author><name>老张同志</name></author><entry><title type="html">ROS 2 自定义接口（msg / srv / action）最小可运行案例（Python）</title><link href="https://1q08.github.io/ros_ws/ros2/tutorial/2026/09/21/ros2-custom-interfaces.html" rel="alternate" type="text/html" title="ROS 2 自定义接口（msg / srv / action）最小可运行案例（Python）" /><published>2026-09-21T03:50:00+00:00</published><updated>2026-09-21T03:50:00+00:00</updated><id>https://1q08.github.io/ros_ws/ros2/tutorial/2026/09/21/ros2-custom-interfaces</id><content type="html" xml:base="https://1q08.github.io/ros_ws/ros2/tutorial/2026/09/21/ros2-custom-interfaces.html"><![CDATA[<h1 id="ros-2-自定义接口msg--srv--action最小可运行案例python">ROS 2 自定义接口（msg / srv / action）最小可运行案例（Python）</h1>

<blockquote>
  <p>一句话目标：<strong>用 <code class="language-plaintext highlighter-rouge">ros2 pkg create</code> 创建一个包，里面同时定义 <code class="language-plaintext highlighter-rouge">.msg</code>、<code class="language-plaintext highlighter-rouge">.srv</code> 和 <code class="language-plaintext highlighter-rouge">.action</code> 三种自定义接口，并各给出一个最小的 Python 收发案例。</strong></p>

  <p>行文顺序：先讲清三个概念（第一节），再定义接口（第三、四、九节），最后写节点（第七、八、九节）。</p>
</blockquote>

<hr />

<h2 id="一什么是-msgsrv-和-action前置知识">一、什么是 msg、srv 和 action（前置知识）</h2>

<p>在动手之前，先搞清楚三个概念：<code class="language-plaintext highlighter-rouge">.msg</code>、<code class="language-plaintext highlighter-rouge">.srv</code> 和 <code class="language-plaintext highlighter-rouge">.action</code> 到底是什么、用来干什么。</p>

<h3 id="11-什么是-msg消息">1.1 什么是 msg（消息）</h3>

<ul>
  <li><strong>msg = message（消息）</strong>，定义节点之间「发布 / 订阅」通信的<strong>数据格式</strong>。</li>
  <li>本质：一份数据结构的”契约”——规定一条消息里有哪些字段、每个字段是什么类型。</li>
  <li>文件扩展名 <code class="language-plaintext highlighter-rouge">.msg</code>，放在包的 <code class="language-plaintext highlighter-rouge">msg/</code> 目录。</li>
  <li>编译后，rosidl 会按它自动生成对应语言的类（本教程是 Python 类，如 <code class="language-plaintext highlighter-rouge">AddressBook</code>），代码里 <code class="language-plaintext highlighter-rouge">from custom_interfaces.msg import AddressBook</code> 后，直接实例化、给字段赋值、<code class="language-plaintext highlighter-rouge">publish()</code> 即可。</li>
  <li><strong>通信模型</strong>：发布者（publisher）把消息发到某个「话题（topic）」，订阅者（subscriber）从话题收。特点是<strong>单向、松耦合、可一对多</strong>（多个订阅者同时收）。</li>
</ul>

<h3 id="12-什么是-srv服务">1.2 什么是 srv（服务）</h3>

<ul>
  <li><strong>srv = service（服务）</strong>，定义节点之间「请求 / 响应」通信的<strong>数据格式</strong>。</li>
  <li>本质：一次”一问一答”的契约，分为两部分：
    <ul>
      <li><strong>请求（Request）</strong>：客户端 → 服务端，要什么；</li>
      <li><strong>响应（Response）</strong>：服务端 → 客户端，回什么。</li>
    </ul>
  </li>
  <li>文件扩展名 <code class="language-plaintext highlighter-rouge">.srv</code>，放在包的 <code class="language-plaintext highlighter-rouge">srv/</code> 目录，中间用一行 <code class="language-plaintext highlighter-rouge">---</code> 把请求和响应隔开。</li>
  <li>编译后生成两个类：<code class="language-plaintext highlighter-rouge">X.Request</code> 和 <code class="language-plaintext highlighter-rouge">X.Response</code>（本教程是 <code class="language-plaintext highlighter-rouge">AddTwoInts.Request</code> / <code class="language-plaintext highlighter-rouge">AddTwoInts.Response</code>）。</li>
  <li><strong>通信模型</strong>：服务端（server）对外提供「服务」，客户端（client）发请求后<strong>等待</strong>并拿到响应。特点是<strong>双向、一问一答、同步完成</strong>。</li>
</ul>

<h3 id="13-什么是-action动作">1.3 什么是 action（动作）</h3>

<ul>
  <li><strong>action</strong>，定义节点之间「目标 / 反馈 / 结果」通信的<strong>数据格式</strong>。</li>
  <li>本质：srv 的”超级版”——服务只能一问一答，action 则服务于<strong>耗时长的任务</strong>：客户端下发一个<strong>目标（Goal）</strong>，执行端一边干活一边回传<strong>反馈（Feedback）</strong>，干完再给最终<strong>结果（Result）</strong>。</li>
  <li>文件扩展名 <code class="language-plaintext highlighter-rouge">.action</code>，放在包的 <code class="language-plaintext highlighter-rouge">action/</code> 目录，用<strong>两个</strong>孤立的 <code class="language-plaintext highlighter-rouge">---</code> 分三段（所以文件里共有<strong>三个</strong> <code class="language-plaintext highlighter-rouge">---</code>）：
    <ul>
      <li><code class="language-plaintext highlighter-rouge">---</code> 之前：Goal（目标），客户端下达什么；</li>
      <li>两个 <code class="language-plaintext highlighter-rouge">---</code> 之间：Result（结果），完成后返回什么；</li>
      <li>第二个 <code class="language-plaintext highlighter-rouge">---</code> 之后：Feedback（反馈），过程中实时回传什么。</li>
    </ul>
  </li>
  <li>编译后生成三个类：<code class="language-plaintext highlighter-rouge">X.Goal</code>、<code class="language-plaintext highlighter-rouge">X.Result</code>、<code class="language-plaintext highlighter-rouge">X.Feedback</code>。</li>
  <li><strong>通信模型</strong>：action 客户端（如导航指令）与 action 服务端（如导航执行器）之间，经历”接受目标 → 边执行边反馈 → 完成给结果”的完整过程。特点是<strong>双向、有过程反馈、异步长任务</strong>。</li>
</ul>

<blockquote>
  <p>形象记忆：</p>
  <ul>
    <li>msg 是「<strong>广播数据</strong>」（如传感器每秒刷一次）；</li>
    <li>srv 是「<strong>一问一答</strong>」（如”2 + 3 等于几？”秒回）；</li>
    <li>action 是「<strong>派活 + 边干边汇报 + 干完交差</strong>」（如”把机器人开到 A 点”，一路报”正在走、还剩 X 米”，到了说”已到达”）。</li>
  </ul>
</blockquote>

<h3 id="14-msg--srv--action-直观对比">1.4 msg / srv / action 直观对比</h3>

<table>
  <thead>
    <tr>
      <th>维度</th>
      <th>msg（消息）</th>
      <th>srv（服务）</th>
      <th>action（动作）</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>全称</td>
      <td>message</td>
      <td>service</td>
      <td>action</td>
    </tr>
    <tr>
      <td>通信模型</td>
      <td>发布 / 订阅</td>
      <td>请求 / 响应</td>
      <td>目标 / 反馈 / 结果</td>
    </tr>
    <tr>
      <td>方向</td>
      <td>单向流</td>
      <td>双向一问一答</td>
      <td>双向、带过程反馈</td>
    </tr>
    <tr>
      <td>文件分隔符</td>
      <td>无</td>
      <td>一行 <code class="language-plaintext highlighter-rouge">---</code></td>
      <td>两个 <code class="language-plaintext highlighter-rouge">---</code>（三段）</td>
    </tr>
    <tr>
      <td>生成的 Python 类</td>
      <td>消息类本身</td>
      <td><code class="language-plaintext highlighter-rouge">X.Request</code> + <code class="language-plaintext highlighter-rouge">X.Response</code></td>
      <td><code class="language-plaintext highlighter-rouge">X.Goal</code> / <code class="language-plaintext highlighter-rouge">X.Result</code> / <code class="language-plaintext highlighter-rouge">X.Feedback</code></td>
    </tr>
    <tr>
      <td>节点端 API</td>
      <td><code class="language-plaintext highlighter-rouge">create_publisher</code> / <code class="language-plaintext highlighter-rouge">create_subscription</code></td>
      <td><code class="language-plaintext highlighter-rouge">create_service</code> / <code class="language-plaintext highlighter-rouge">create_client</code></td>
      <td><code class="language-plaintext highlighter-rouge">ActionServer</code> / <code class="language-plaintext highlighter-rouge">ActionClient</code></td>
    </tr>
    <tr>
      <td>典型场景</td>
      <td>传感器数据流</td>
      <td>查询 / 计算（如加两个数）</td>
      <td>移动到位、机械臂抓取（长任务）</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>一句话记忆：<strong>msg 是”广播数据”，srv 是”一问一答”，action 是”带进度反馈的长任务”。</strong></p>
</blockquote>

<h3 id="15-msg--srv--action-是如何调用的">1.5 msg / srv / action 是如何调用的？</h3>

<p>下面先讲清”调用套路”，完整代码见本教程第七、八、九节，对照着看就一目了然。</p>

<h4 id="-调用-msg发布端填字段--发订阅端回调里收">① 调用 msg：发布端”填字段 → 发”，订阅端”回调里收”</h4>

<p><strong>发布端（三步）</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="bp">self</span><span class="p">.</span><span class="n">publisher</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">create_publisher</span><span class="p">(</span><span class="n">AddressBook</span><span class="p">,</span> <span class="s">'address_book'</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span>  <span class="c1"># ① 声明类型 + 话题
</span><span class="n">msg</span> <span class="o">=</span> <span class="n">AddressBook</span><span class="p">()</span>                        <span class="c1"># ② 实例化消息对象
</span><span class="n">msg</span><span class="p">.</span><span class="n">first_name</span> <span class="o">=</span> <span class="s">'John'</span>                    <span class="c1">#    给字段赋值
</span><span class="n">msg</span><span class="p">.</span><span class="n">phone_type</span> <span class="o">=</span> <span class="n">AddressBook</span><span class="p">.</span><span class="n">PHONE_TYPE_MOBILE</span>   <span class="c1"># 常量用类名引用
</span><span class="bp">self</span><span class="p">.</span><span class="n">publisher</span><span class="p">.</span><span class="n">publish</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span>                <span class="c1"># ③ 发布
</span></code></pre></div></div>

<p><strong>订阅端（两步）</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="bp">self</span><span class="p">.</span><span class="n">create_subscription</span><span class="p">(</span><span class="n">AddressBook</span><span class="p">,</span> <span class="s">'address_book'</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">callback</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span>  <span class="c1"># ① 订阅
</span>
<span class="k">def</span> <span class="nf">callback</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">msg</span><span class="p">):</span>                   <span class="c1"># ② 回调里直接读字段
</span>    <span class="k">print</span><span class="p">(</span><span class="n">msg</span><span class="p">.</span><span class="n">first_name</span><span class="p">,</span> <span class="n">msg</span><span class="p">.</span><span class="n">phone_type</span><span class="p">)</span>
</code></pre></div></div>

<h4 id="-调用-srv客户端填请求--异步发服务端回调里算--回响应">② 调用 srv：客户端”填请求 → 异步发”，服务端”回调里算 → 回响应”</h4>

<p><strong>服务端（两步）</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="bp">self</span><span class="p">.</span><span class="n">srv</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">create_service</span><span class="p">(</span><span class="n">AddTwoInts</span><span class="p">,</span> <span class="s">'add_two_ints'</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">callback</span><span class="p">)</span>  <span class="c1"># ① 声明服务
</span>
<span class="k">def</span> <span class="nf">callback</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="n">response</span><span class="p">):</span>     <span class="c1"># ② 读请求、填响应、return
</span>    <span class="n">response</span><span class="p">.</span><span class="nb">sum</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">a</span> <span class="o">+</span> <span class="n">request</span><span class="p">.</span><span class="n">b</span>
    <span class="k">return</span> <span class="n">response</span>
</code></pre></div></div>

<p><strong>客户端（三步）</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="bp">self</span><span class="p">.</span><span class="n">cli</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">create_client</span><span class="p">(</span><span class="n">AddTwoInts</span><span class="p">,</span> <span class="s">'add_two_ints'</span><span class="p">)</span>      <span class="c1"># ① 创建客户端
</span><span class="k">while</span> <span class="ow">not</span> <span class="bp">self</span><span class="p">.</span><span class="n">cli</span><span class="p">.</span><span class="n">wait_for_service</span><span class="p">(</span><span class="n">timeout_sec</span><span class="o">=</span><span class="mf">1.0</span><span class="p">):</span> <span class="p">...</span>       <span class="c1">#    等服务端上线
</span><span class="n">req</span> <span class="o">=</span> <span class="n">AddTwoInts</span><span class="p">.</span><span class="n">Request</span><span class="p">()</span>                                      <span class="c1"># ② 构造请求对象
</span><span class="n">req</span><span class="p">.</span><span class="n">a</span> <span class="o">=</span> <span class="mi">2</span>
<span class="n">req</span><span class="p">.</span><span class="n">b</span> <span class="o">=</span> <span class="mi">3</span>                                                       <span class="c1">#    填请求字段
</span><span class="n">future</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">cli</span><span class="p">.</span><span class="n">call_async</span><span class="p">(</span><span class="n">req</span><span class="p">)</span>                               <span class="c1"># ③ 异步发送
</span><span class="n">rclpy</span><span class="p">.</span><span class="n">spin_once</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span>                                           <span class="c1">#    轮询等结果
</span><span class="n">response</span> <span class="o">=</span> <span class="n">future</span><span class="p">.</span><span class="n">result</span><span class="p">()</span>                                      <span class="c1">#    拿响应 response.sum
</span></code></pre></div></div>

<h4 id="-调用-action客户端发目标--等接受--拿结果服务端接受--边反馈边执行--给结果">③ 调用 action：客户端”发目标 → 等接受 → 拿结果”，服务端”接受 → 边反馈边执行 → 给结果”</h4>

<p><strong>服务端（三步）</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">rclpy.action</span> <span class="kn">import</span> <span class="n">ActionServer</span>                     <span class="c1"># 导入 action 专用 API
</span>
<span class="bp">self</span><span class="p">.</span><span class="n">action_server</span> <span class="o">=</span> <span class="n">ActionServer</span><span class="p">(</span>                        <span class="c1"># ① 声明动作（目标回调去接受 / 执行）
</span>    <span class="bp">self</span><span class="p">,</span> <span class="n">Fibonacci</span><span class="p">,</span> <span class="s">'fibonacci'</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">execute_callback</span><span class="p">)</span>  <span class="c1">#    三个参数：类型、动作名、回调
</span>
<span class="k">def</span> <span class="nf">execute_callback</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">goal_handle</span><span class="p">):</span>                  <span class="c1"># ② 处理目标、循环回传反馈
</span>    <span class="n">goal_handle</span><span class="p">.</span><span class="n">publish_feedback</span><span class="p">(</span><span class="n">feedback</span><span class="p">)</span>                <span class="c1">#    执行中实时发 Feedback
</span>    <span class="n">goal_handle</span><span class="p">.</span><span class="n">succeed</span><span class="p">()</span>                                 <span class="c1">#    标记成功 / 失败
</span>    <span class="k">return</span> <span class="n">result</span>                                         <span class="c1"># ③ return 最终 Result
</span></code></pre></div></div>

<p><strong>客户端（三步）</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="bp">self</span><span class="p">.</span><span class="n">action_client</span> <span class="o">=</span> <span class="n">ActionClient</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">Fibonacci</span><span class="p">,</span> <span class="s">'fibonacci'</span><span class="p">)</span>   <span class="c1"># ① 创建动作客户端
</span><span class="bp">self</span><span class="p">.</span><span class="n">action_client</span><span class="p">.</span><span class="n">wait_for_server</span><span class="p">()</span>                              <span class="c1">#    等服务端上线
</span>
<span class="n">goal_msg</span> <span class="o">=</span> <span class="n">Fibonacci</span><span class="p">.</span><span class="n">Goal</span><span class="p">()</span>                                       <span class="c1"># ② 构造目标对象
</span><span class="n">goal_msg</span><span class="p">.</span><span class="n">order</span> <span class="o">=</span> <span class="mi">5</span>

<span class="n">future</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">action_client</span><span class="p">.</span><span class="n">send_goal_async</span><span class="p">(</span>                      <span class="c1"># ③ 异步发目标
</span>    <span class="n">goal_msg</span><span class="p">,</span> <span class="n">feedback_callback</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">on_feedback</span><span class="p">)</span>                 <span class="c1">#    注册"反馈"回调
</span>
<span class="n">rclpy</span><span class="p">.</span><span class="n">spin_until_future_complete</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">future</span><span class="p">)</span>                    <span class="c1">#    先等「目标是否被接受」
</span><span class="n">goal_handle</span> <span class="o">=</span> <span class="n">future</span><span class="p">.</span><span class="n">result</span><span class="p">()</span>
<span class="k">if</span> <span class="n">goal_handle</span><span class="p">.</span><span class="n">accepted</span><span class="p">:</span>
    <span class="n">result_future</span> <span class="o">=</span> <span class="n">goal_handle</span><span class="p">.</span><span class="n">get_result_async</span><span class="p">()</span>                <span class="c1">#    接受后再等「最终结果」
</span>    <span class="n">rclpy</span><span class="p">.</span><span class="n">spin_until_future_complete</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">result_future</span><span class="p">)</span>
    <span class="n">result</span> <span class="o">=</span> <span class="n">result_future</span><span class="p">.</span><span class="n">result</span><span class="p">().</span><span class="n">result</span>
</code></pre></div></div>

<h4 id="-关键接口的字段从哪来requesta--responsesum--goalorder-的来源">④ 关键：接口的字段从哪来？（<code class="language-plaintext highlighter-rouge">request.a</code> / <code class="language-plaintext highlighter-rouge">response.sum</code> / <code class="language-plaintext highlighter-rouge">goal.order</code> 的来源）</h4>

<p>在代码里访问 <code class="language-plaintext highlighter-rouge">request.a</code>、<code class="language-plaintext highlighter-rouge">response.sum</code>、<code class="language-plaintext highlighter-rouge">msg.first_name</code>、<code class="language-plaintext highlighter-rouge">goal.order</code> 时，<strong>字段名不是随便起的，而是由接口文件严格定义的</strong>。</p>

<p>以 <code class="language-plaintext highlighter-rouge">srv/AddTwoInts.srv</code> 为例：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>int64 a
int64 b
---
int64 sum
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">---</code> <strong>上面</strong>是请求字段 → <code class="language-plaintext highlighter-rouge">Request</code> 对象有 <code class="language-plaintext highlighter-rouge">a</code>、<code class="language-plaintext highlighter-rouge">b</code></li>
  <li><code class="language-plaintext highlighter-rouge">---</code> <strong>下面</strong>是响应字段 → <code class="language-plaintext highlighter-rouge">Response</code> 对象有 <code class="language-plaintext highlighter-rouge">sum</code></li>
</ul>

<p>字段名是<strong>硬绑定</strong>的，用别的会报错：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">response</span><span class="p">.</span><span class="nb">sum</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">a</span> <span class="o">+</span> <span class="n">request</span><span class="p">.</span><span class="n">b</span>   <span class="c1"># ✅ a、b、sum 都是 srv 里定义的
</span><span class="n">response</span><span class="p">.</span><span class="nb">sum</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">x</span> <span class="o">+</span> <span class="n">request</span><span class="p">.</span><span class="n">y</span>   <span class="c1"># ❌ AttributeError: no attribute 'x'
</span></code></pre></div></div>

<blockquote>
  <ul>
    <li>想用别的字段名怎么办？→ 改自己的接口文件（本教程第三、四、九节）。</li>
    <li>不确定某个接口有哪些字段？→ <code class="language-plaintext highlighter-rouge">ros2 interface show</code> 直接打印。</li>
  </ul>
</blockquote>

<p>action 同理，三段分别对应 <code class="language-plaintext highlighter-rouge">Fibonacci.Goal</code> / <code class="language-plaintext highlighter-rouge">Fibonacci.Result</code> / <code class="language-plaintext highlighter-rouge">Fibonacci.Feedback</code>：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">goal_msg</span><span class="p">.</span><span class="n">order</span> <span class="o">=</span> <span class="mi">5</span>                       <span class="c1"># Goal 段字段：order
</span><span class="n">result</span><span class="p">.</span><span class="n">sequence</span> <span class="o">=</span> <span class="p">...</span>                    <span class="c1"># Result 段字段：sequence
</span><span class="n">feedback</span><span class="p">.</span><span class="n">partial_sequence</span> <span class="o">=</span> <span class="p">...</span>          <span class="c1"># Feedback 段字段：partial_sequence
</span></code></pre></div></div>

<h4 id="-完整调用链路">⑤ 完整调用链路</h4>

<pre><code class="language-mermaid">flowchart LR
    A[".msg / .srv / .action&lt;br/&gt;文本定义字段"] --&gt; B["rosidl_generate_interfaces&lt;br/&gt;(CMake 编译期)"]
    B --&gt; C["生成 Python 类&lt;br/&gt;custom_interfaces.msg / .srv / .action"]
    C --&gt; D["节点里 import + 调用"]
    D --&gt; E["msg: publish 广播&lt;br/&gt;srv: call_async 请求&lt;br/&gt;action: send_goal_async 发目标"]
    E --&gt; F["对端: 回调收 / 回响应 / 回反馈给结果"]
</code></pre>

<h3 id="16-环境信息">1.6 环境信息</h3>

<ul>
  <li>ROS 2 发行版：Jazzy（<code class="language-plaintext highlighter-rouge">/opt/ros/jazzy</code>）</li>
  <li>工作空间：<code class="language-plaintext highlighter-rouge">~/ros_ws</code></li>
  <li>💡 <strong>小提示</strong>：如果运行节点或 <code class="language-plaintext highlighter-rouge">ros2 topic echo</code> 时发现话题 / 服务搜不到，多半是所处 WiFi 环境的组播发现被拦截了，可以在运行前先限定为本机发现：</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">ROS_AUTOMATIC_DISCOVERY_RANGE</span><span class="o">=</span>LOCALHOST
</code></pre></div></div>

<h3 id="17-为什么自定义接口的包必须用-ament_cmake-构建">1.7 为什么自定义接口的包必须用 <code class="language-plaintext highlighter-rouge">ament_cmake</code> 构建？</h3>

<table>
  <thead>
    <tr>
      <th>构建方式</th>
      <th>能否定义接口</th>
      <th>说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ament_python</code></td>
      <td>❌ 不能</td>
      <td>只能写 Python 节点，它不会调用 rosidl 生成器</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ament_cmake</code></td>
      <td>✅ 能</td>
      <td><code class="language-plaintext highlighter-rouge">rosidl_generate_interfaces</code> 只能在 CMake 包中使用</td>
    </tr>
  </tbody>
</table>

<p>所以接口包<strong>必须</strong>是 <code class="language-plaintext highlighter-rouge">ament_cmake</code>，哪怕里面全是 Python 代码（本案例即是如此）。</p>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">.msg</code> / <code class="language-plaintext highlighter-rouge">.srv</code> / <code class="language-plaintext highlighter-rouge">.action</code> 三种接口<strong>都要走 <code class="language-plaintext highlighter-rouge">rosidl_generate_interfaces</code> 生成</strong>，因此定义 action 的包同样是 <code class="language-plaintext highlighter-rouge">ament_cmake</code>（参见 9.2 在 CMakeLists 里加 <code class="language-plaintext highlighter-rouge">.action</code>）。</p>
</blockquote>

<blockquote>
  <p>补课：<code class="language-plaintext highlighter-rouge">ament_cmake_python</code> 不是构建类型（它不是 <code class="language-plaintext highlighter-rouge">--build-type</code> 的可选项），而是 ament_cmake 的一个扩展，提供 <code class="language-plaintext highlighter-rouge">ament_python_install_package()</code> 等函数，用得不多，本教程不使用它。</p>
</blockquote>

<hr />

<h2 id="二用-ros2-pkg-create-创建包">二、用 <code class="language-plaintext highlighter-rouge">ros2 pkg create</code> 创建包</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/ros_ws/src
ros2 pkg create <span class="nt">--build-type</span> ament_cmake <span class="nt">--license</span> Apache-2.0 custom_interfaces
</code></pre></div></div>

<p>生成骨架：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>custom_interfaces/
├── CMakeLists.txt
├── package.xml
├── LICENSE
├── include/custom_interfaces/   # 本案例不需要，可删除
└── src/                         # 本案例不需要，可删除
</code></pre></div></div>

<p>接着手工创建接口与脚本目录：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>custom_interfaces
<span class="nb">mkdir </span>msg srv scripts launch
<span class="nb">rm</span> <span class="nt">-rf</span> include src
</code></pre></div></div>

<hr />

<h2 id="三定义-msg-接口消息">三、定义 <code class="language-plaintext highlighter-rouge">.msg</code> 接口（消息）</h2>

<p>文件：<code class="language-plaintext highlighter-rouge">msg/AddressBook.msg</code></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># 常量：类型 名字 = 值（用大写表示，属于消息类）
uint8 PHONE_TYPE_HOME=0
uint8 PHONE_TYPE_WORK=1
uint8 PHONE_TYPE_MOBILE=2

# 字段：类型 名字
string first_name
string last_name
string phone_number
uint8 phone_type
</code></pre></div></div>

<p><strong>字段类型速查（常用）</strong>：</p>

<table>
  <thead>
    <tr>
      <th>类型</th>
      <th>说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bool</code> / <code class="language-plaintext highlighter-rouge">byte</code> / <code class="language-plaintext highlighter-rouge">int8</code>~<code class="language-plaintext highlighter-rouge">int64</code> / <code class="language-plaintext highlighter-rouge">uint8</code>~<code class="language-plaintext highlighter-rouge">uint64</code></td>
      <td>基本数值</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">float32</code> / <code class="language-plaintext highlighter-rouge">float64</code></td>
      <td>浮点</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">string</code></td>
      <td>字符串</td>
    </tr>
    <tr>
      <td>其他消息类型（如 <code class="language-plaintext highlighter-rouge">std_msgs/Header</code>）</td>
      <td>嵌套</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Type[]</code> 或 <code class="language-plaintext highlighter-rouge">Type[N]</code></td>
      <td>数组 / 定长数组</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="四定义-srv-接口服务">四、定义 <code class="language-plaintext highlighter-rouge">.srv</code> 接口（服务）</h2>

<p>文件：<code class="language-plaintext highlighter-rouge">srv/AddTwoInts.srv</code></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># ==== `---` 上面是请求（Request）字段 ====
int64 a
int64 b
---
# ==== `---` 下面是响应（Response）字段 ====
int64 sum
</code></pre></div></div>

<blockquote>
  <p>关键：<code class="language-plaintext highlighter-rouge">.srv</code> 里<strong>必须有一个孤立的 <code class="language-plaintext highlighter-rouge">---</code></strong> 把请求和响应隔开；生成的 Python 类分别是 <code class="language-plaintext highlighter-rouge">AddTwoInts.Request</code> 和 <code class="language-plaintext highlighter-rouge">AddTwoInts.Response</code>。</p>
</blockquote>

<hr />

<h2 id="五配置-cmakeliststxt核心">五、配置 CMakeLists.txt（核心）</h2>

<div class="language-cmake highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cmake_minimum_required</span><span class="p">(</span>VERSION 3.8<span class="p">)</span>
<span class="nb">project</span><span class="p">(</span>custom_interfaces<span class="p">)</span>

<span class="nb">find_package</span><span class="p">(</span>ament_cmake REQUIRED<span class="p">)</span>                  <span class="c1"># 构建工具</span>
<span class="nb">find_package</span><span class="p">(</span>rosidl_default_generators REQUIRED<span class="p">)</span>    <span class="c1"># 接口代码生成器</span>

<span class="c1"># 声明并生成接口（msg / srv / action 一起列出）</span>
<span class="nf">rosidl_generate_interfaces</span><span class="p">(</span><span class="si">${</span><span class="nv">PROJECT_NAME</span><span class="si">}</span>
  <span class="s2">"msg/AddressBook.msg"</span>
  <span class="s2">"srv/AddTwoInts.srv"</span>
  <span class="s2">"action/Fibonacci.action"</span>
<span class="p">)</span>

<span class="c1"># 导出接口运行时依赖（供别的包使用）</span>
<span class="nf">ament_export_dependencies</span><span class="p">(</span>rosidl_default_runtime<span class="p">)</span>

<span class="c1"># 安装 Python 脚本</span>
<span class="nb">install</span><span class="p">(</span>PROGRAMS
  scripts/publish_address_book.py
  scripts/subscribe_address_book.py
  scripts/add_two_ints_server.py
  scripts/add_two_ints_client.py
  DESTINATION lib/<span class="si">${</span><span class="nv">PROJECT_NAME</span><span class="si">}</span><span class="p">)</span>

<span class="c1"># 安装 launch 文件</span>
<span class="nb">install</span><span class="p">(</span>DIRECTORY launch
  DESTINATION share/<span class="si">${</span><span class="nv">PROJECT_NAME</span><span class="si">}</span><span class="p">)</span>

<span class="nb">if</span><span class="p">(</span>BUILD_TESTING<span class="p">)</span>
  <span class="c1"># pkg create 自动生成，保留即可</span>
<span class="nb">endif</span><span class="p">()</span>

<span class="nf">ament_package</span><span class="p">()</span>
</code></pre></div></div>

<p><strong>核心要点只有一句</strong>：<code class="language-plaintext highlighter-rouge">rosidl_generate_interfaces(${PROJECT_NAME} &lt;接口文件&gt;)</code>。它会自动生成 Python / C++ 等所有语言的消息代码。</p>

<blockquote>
  <p>这里把三种接口一次性都列上了，其中 <code class="language-plaintext highlighter-rouge">action/Fibonacci.action</code> 文件要到 9.1 节才创建。如果你只想先跑 msg / srv 两个案例，可以先去掉这一行，等学到第九节再加上。</p>
</blockquote>

<hr />

<h2 id="六配置-packagexml">六、配置 package.xml</h2>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;package</span> <span class="na">format=</span><span class="s">"3"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;name&gt;</span>custom_interfaces<span class="nt">&lt;/name&gt;</span>
  <span class="nt">&lt;version&gt;</span>0.0.0<span class="nt">&lt;/version&gt;</span>
  <span class="nt">&lt;description&gt;</span>同时定义 msg、srv 与 action 接口的 Python 最小示例包<span class="nt">&lt;/description&gt;</span>
  <span class="nt">&lt;maintainer</span> <span class="na">email=</span><span class="s">"your_email@example.com"</span><span class="nt">&gt;</span>your_name<span class="nt">&lt;/maintainer&gt;</span>
  <span class="nt">&lt;license&gt;</span>Apache-2.0<span class="nt">&lt;/license&gt;</span>

  <span class="nt">&lt;buildtool_depend&gt;</span>ament_cmake<span class="nt">&lt;/buildtool_depend&gt;</span>                 <span class="c">&lt;!-- 构建工具 --&gt;</span>
  <span class="nt">&lt;buildtool_depend&gt;</span>rosidl_default_generators<span class="nt">&lt;/buildtool_depend&gt;</span>   <span class="c">&lt;!-- 接口生成器 --&gt;</span>
  <span class="nt">&lt;exec_depend&gt;</span>rosidl_default_runtime<span class="nt">&lt;/exec_depend&gt;</span>                <span class="c">&lt;!-- 接口运行时 --&gt;</span>
  <span class="nt">&lt;exec_depend&gt;</span>rclpy<span class="nt">&lt;/exec_depend&gt;</span>                                 <span class="c">&lt;!-- Python 节点 --&gt;</span>
  <span class="nt">&lt;member_of_group&gt;</span>rosidl_interface_packages<span class="nt">&lt;/member_of_group&gt;</span>     <span class="c">&lt;!-- 接口包分组 --&gt;</span>

  <span class="nt">&lt;test_depend&gt;</span>ament_lint_auto<span class="nt">&lt;/test_depend&gt;</span>
  <span class="nt">&lt;test_depend&gt;</span>ament_lint_common<span class="nt">&lt;/test_depend&gt;</span>

  <span class="nt">&lt;export&gt;</span>
    <span class="nt">&lt;build_type&gt;</span>ament_cmake<span class="nt">&lt;/build_type&gt;</span>
  <span class="nt">&lt;/export&gt;</span>
<span class="nt">&lt;/package&gt;</span>
</code></pre></div></div>

<p>依赖项缺一不可：</p>

<table>
  <thead>
    <tr>
      <th>标签</th>
      <th>作用</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">buildtool_depend: ament_cmake</code></td>
      <td>构建工具</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">buildtool_depend: rosidl_default_generators</code></td>
      <td>接口生成器</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">exec_depend: rosidl_default_runtime</code></td>
      <td>运行期加载接口</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">exec_depend: rclpy</code></td>
      <td>Python 客户端库</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">member_of_group: rosidl_interface_packages</code></td>
      <td>声明”我是接口包”</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="七案例一消息-pub--subpython">七、案例一：消息 pub / sub（Python）</h2>

<h3 id="71-发布者-scriptspublish_address_bookpy">7.1 发布者 <code class="language-plaintext highlighter-rouge">scripts/publish_address_book.py</code></h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python3
</span><span class="kn">import</span> <span class="nn">rclpy</span>
<span class="kn">from</span> <span class="nn">rclpy.node</span> <span class="kn">import</span> <span class="n">Node</span>
<span class="kn">from</span> <span class="nn">custom_interfaces.msg</span> <span class="kn">import</span> <span class="n">AddressBook</span>   <span class="c1"># 由 .msg 生成的类型
</span>

<span class="k">class</span> <span class="nc">AddressBookPublisher</span><span class="p">(</span><span class="n">Node</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="s">'address_book_publisher'</span><span class="p">)</span>
        <span class="c1"># create_publisher(消息类型, 话题名, 队列长度)
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">publisher</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">create_publisher</span><span class="p">(</span><span class="n">AddressBook</span><span class="p">,</span> <span class="s">'address_book'</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span>
        <span class="n">timer_period</span> <span class="o">=</span> <span class="mf">1.0</span>   <span class="c1"># 每 1 秒发一次
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">timer</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">create_timer</span><span class="p">(</span><span class="n">timer_period</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">timer_callback</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">timer_callback</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="n">msg</span> <span class="o">=</span> <span class="n">AddressBook</span><span class="p">()</span>   <span class="c1"># 直接实例化消息对象
</span>        <span class="n">msg</span><span class="p">.</span><span class="n">first_name</span> <span class="o">=</span> <span class="s">'John'</span>
        <span class="n">msg</span><span class="p">.</span><span class="n">last_name</span> <span class="o">=</span> <span class="s">'Doe'</span>
        <span class="n">msg</span><span class="p">.</span><span class="n">phone_number</span> <span class="o">=</span> <span class="s">'1234567890'</span>
        <span class="n">msg</span><span class="p">.</span><span class="n">phone_type</span> <span class="o">=</span> <span class="n">AddressBook</span><span class="p">.</span><span class="n">PHONE_TYPE_MOBILE</span>   <span class="c1"># 常量用类名引用
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">publisher</span><span class="p">.</span><span class="n">publish</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">get_logger</span><span class="p">().</span><span class="n">info</span><span class="p">(</span><span class="s">'Publishing Contact</span><span class="se">\n</span><span class="s">First:%s Last:%s'</span>
                               <span class="o">%</span> <span class="p">(</span><span class="n">msg</span><span class="p">.</span><span class="n">first_name</span><span class="p">,</span> <span class="n">msg</span><span class="p">.</span><span class="n">last_name</span><span class="p">))</span>


<span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">init</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="n">args</span><span class="p">)</span>
    <span class="n">node</span> <span class="o">=</span> <span class="n">AddressBookPublisher</span><span class="p">()</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">spin</span><span class="p">(</span><span class="n">node</span><span class="p">)</span>
    <span class="n">node</span><span class="p">.</span><span class="n">destroy_node</span><span class="p">()</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">shutdown</span><span class="p">()</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="72-订阅者-scriptssubscribe_address_bookpy">7.2 订阅者 <code class="language-plaintext highlighter-rouge">scripts/subscribe_address_book.py</code></h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python3
</span><span class="kn">import</span> <span class="nn">rclpy</span>
<span class="kn">from</span> <span class="nn">rclpy.node</span> <span class="kn">import</span> <span class="n">Node</span>
<span class="kn">from</span> <span class="nn">custom_interfaces.msg</span> <span class="kn">import</span> <span class="n">AddressBook</span>


<span class="k">class</span> <span class="nc">AddressBookSubscriber</span><span class="p">(</span><span class="n">Node</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="s">'address_book_subscriber'</span><span class="p">)</span>
        <span class="c1"># create_subscription(消息类型, 话题名, 回调, 队列长度)
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">subscription</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">create_subscription</span><span class="p">(</span>
            <span class="n">AddressBook</span><span class="p">,</span> <span class="s">'address_book'</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">listener_callback</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">listener_callback</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">msg</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">get_logger</span><span class="p">().</span><span class="n">info</span><span class="p">(</span>
            <span class="s">'I heard:</span><span class="se">\n</span><span class="s">  First:%s Last:%s</span><span class="se">\n</span><span class="s">  Phone:%s Type:%s'</span>
            <span class="o">%</span> <span class="p">(</span><span class="n">msg</span><span class="p">.</span><span class="n">first_name</span><span class="p">,</span> <span class="n">msg</span><span class="p">.</span><span class="n">last_name</span><span class="p">,</span>
               <span class="n">msg</span><span class="p">.</span><span class="n">phone_number</span><span class="p">,</span> <span class="n">msg</span><span class="p">.</span><span class="n">phone_type</span><span class="p">))</span>


<span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">init</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="n">args</span><span class="p">)</span>
    <span class="n">node</span> <span class="o">=</span> <span class="n">AddressBookSubscriber</span><span class="p">()</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">spin</span><span class="p">(</span><span class="n">node</span><span class="p">)</span>
    <span class="n">node</span><span class="p">.</span><span class="n">destroy_node</span><span class="p">()</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">shutdown</span><span class="p">()</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<hr />

<h2 id="八案例二服务-server--clientpython">八、案例二：服务 server / client（Python）</h2>

<h3 id="81-服务端-scriptsadd_two_ints_serverpy">8.1 服务端 <code class="language-plaintext highlighter-rouge">scripts/add_two_ints_server.py</code></h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python3
</span><span class="kn">import</span> <span class="nn">rclpy</span>
<span class="kn">from</span> <span class="nn">rclpy.node</span> <span class="kn">import</span> <span class="n">Node</span>
<span class="kn">from</span> <span class="nn">custom_interfaces.srv</span> <span class="kn">import</span> <span class="n">AddTwoInts</span>   <span class="c1"># 生成的服务类型
</span>

<span class="k">class</span> <span class="nc">AddTwoIntsServer</span><span class="p">(</span><span class="n">Node</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="s">'add_two_ints_server'</span><span class="p">)</span>
        <span class="c1"># create_service(服务类型, 服务名, 回调)
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">srv</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">create_service</span><span class="p">(</span>
            <span class="n">AddTwoInts</span><span class="p">,</span> <span class="s">'add_two_ints'</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">add_two_ints_callback</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">add_two_ints_callback</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="n">response</span><span class="p">):</span>
        <span class="c1"># request.a / request.b 是请求字段
</span>        <span class="n">response</span><span class="p">.</span><span class="nb">sum</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">a</span> <span class="o">+</span> <span class="n">request</span><span class="p">.</span><span class="n">b</span>   <span class="c1"># 填响应字段
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">get_logger</span><span class="p">().</span><span class="n">info</span><span class="p">(</span>
            <span class="s">'Incoming request</span><span class="se">\n</span><span class="s">a: %d  b: %d'</span> <span class="o">%</span> <span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="n">a</span><span class="p">,</span> <span class="n">request</span><span class="p">.</span><span class="n">b</span><span class="p">))</span>
        <span class="k">return</span> <span class="n">response</span>


<span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">init</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="n">args</span><span class="p">)</span>
    <span class="n">node</span> <span class="o">=</span> <span class="n">AddTwoIntsServer</span><span class="p">()</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">spin</span><span class="p">(</span><span class="n">node</span><span class="p">)</span>
    <span class="n">node</span><span class="p">.</span><span class="n">destroy_node</span><span class="p">()</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">shutdown</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="82-客户端-scriptsadd_two_ints_clientpy">8.2 客户端 <code class="language-plaintext highlighter-rouge">scripts/add_two_ints_client.py</code></h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python3
</span><span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">import</span> <span class="nn">rclpy</span>
<span class="kn">from</span> <span class="nn">rclpy.node</span> <span class="kn">import</span> <span class="n">Node</span>
<span class="kn">from</span> <span class="nn">custom_interfaces.srv</span> <span class="kn">import</span> <span class="n">AddTwoInts</span>


<span class="k">class</span> <span class="nc">AddTwoIntsClient</span><span class="p">(</span><span class="n">Node</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="s">'add_two_ints_client'</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">cli</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">create_client</span><span class="p">(</span><span class="n">AddTwoInts</span><span class="p">,</span> <span class="s">'add_two_ints'</span><span class="p">)</span>
        <span class="c1"># 等待服务端上线
</span>        <span class="k">while</span> <span class="ow">not</span> <span class="bp">self</span><span class="p">.</span><span class="n">cli</span><span class="p">.</span><span class="n">wait_for_service</span><span class="p">(</span><span class="n">timeout_sec</span><span class="o">=</span><span class="mf">1.0</span><span class="p">):</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">get_logger</span><span class="p">().</span><span class="n">info</span><span class="p">(</span><span class="s">'service not available, waiting again...'</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">req</span> <span class="o">=</span> <span class="n">AddTwoInts</span><span class="p">.</span><span class="n">Request</span><span class="p">()</span>

    <span class="k">def</span> <span class="nf">send_request</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">req</span><span class="p">.</span><span class="n">a</span> <span class="o">=</span> <span class="n">a</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">req</span><span class="p">.</span><span class="n">b</span> <span class="o">=</span> <span class="n">b</span>
        <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">cli</span><span class="p">.</span><span class="n">call_async</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">req</span><span class="p">)</span>   <span class="c1"># 异步调用，返回 future
</span>

<span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">init</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="n">args</span><span class="p">)</span>
    <span class="n">client</span> <span class="o">=</span> <span class="n">AddTwoIntsClient</span><span class="p">()</span>
    <span class="n">a</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span> <span class="k">else</span> <span class="mi">2</span>
    <span class="n">b</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">2</span> <span class="k">else</span> <span class="mi">3</span>
    <span class="n">future</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">send_request</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span>
    <span class="k">while</span> <span class="n">rclpy</span><span class="p">.</span><span class="n">ok</span><span class="p">():</span>
        <span class="n">rclpy</span><span class="p">.</span><span class="n">spin_once</span><span class="p">(</span><span class="n">client</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">future</span><span class="p">.</span><span class="n">done</span><span class="p">():</span>
            <span class="k">try</span><span class="p">:</span>
                <span class="n">response</span> <span class="o">=</span> <span class="n">future</span><span class="p">.</span><span class="n">result</span><span class="p">()</span>
                <span class="k">print</span><span class="p">(</span><span class="s">'%d + %d = %d'</span> <span class="o">%</span> <span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">response</span><span class="p">.</span><span class="nb">sum</span><span class="p">))</span>
            <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
                <span class="k">print</span><span class="p">(</span><span class="s">'Service call failed: %r'</span> <span class="o">%</span> <span class="n">e</span><span class="p">)</span>
            <span class="k">break</span>
    <span class="n">client</span><span class="p">.</span><span class="n">destroy_node</span><span class="p">()</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">shutdown</span><span class="p">()</span>
</code></pre></div></div>

<hr />

<h2 id="九案例三action-服务端--客户端python">九、案例三：action 服务端 / 客户端（Python）</h2>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">.msg</code> 是”广播数据”，<code class="language-plaintext highlighter-rouge">.srv</code> 是”一问一答”，<code class="language-plaintext highlighter-rouge">.action</code> 则是”<strong>带过程反馈的长任务</strong>“——下发目标、边干边反馈进度、最后给结果。适合导航、机械臂抓取这类耗时动作。</p>
</blockquote>

<h3 id="91-定义-action-接口动作">9.1 定义 <code class="language-plaintext highlighter-rouge">.action</code> 接口（动作）</h3>

<p><code class="language-plaintext highlighter-rouge">.action</code> 文件放在包根目录的 <code class="language-plaintext highlighter-rouge">action/</code> 子目录（不是 <code class="language-plaintext highlighter-rouge">msg/</code> 或 <code class="language-plaintext highlighter-rouge">srv/</code>），用<strong>三个</strong>孤立 <code class="language-plaintext highlighter-rouge">---</code> 把内容分成三段，从上到下依次是 <strong>Goal（目标）/ Result（结果）/ Feedback（反馈）</strong>。</p>

<p>文件：<code class="language-plaintext highlighter-rouge">action/Fibonacci.action</code></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># ===== 第 1 段：Goal（目标）——客户端下达的目标 =====
int32 order

---
# ===== 第 2 段：Result（结果）——动作完成后的最终结果 =====
int32[] sequence

---
# ===== 第 3 段：Feedback（反馈）——执行过程中的阶段性信息 =====
int32[] partial_sequence
</code></pre></div></div>

<blockquote>
  <p>关键认知（对应第十二节的对比表）：</p>
  <ul>
    <li><code class="language-plaintext highlighter-rouge">.action</code> 用<strong>三个</strong> <code class="language-plaintext highlighter-rouge">---</code>（生成 <code class="language-plaintext highlighter-rouge">X.Goal</code> / <code class="language-plaintext highlighter-rouge">X.Result</code> / <code class="language-plaintext highlighter-rouge">X.Feedback</code> 三类）；</li>
    <li>它是 <code class="language-plaintext highlighter-rouge">srv</code> 的「超级版」：Goal 段相当于请求、Result 段相当于响应，额外多一个 Feedback 段实时回传进度；</li>
    <li>底层实现上，ROS 2 会把一个 <code class="language-plaintext highlighter-rouge">.action</code> 自动展开成 <strong>3 个 topic + 2 个 service</strong>（见 9.5），但写代码时你无需关心这些细节。</li>
  </ul>
</blockquote>

<h3 id="92-在-cmakeliststxt-里加上-action">9.2 在 <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> 里加上 <code class="language-plaintext highlighter-rouge">.action</code></h3>

<p>在第五节的 <code class="language-plaintext highlighter-rouge">rosidl_generate_interfaces(...)</code> 里，把 <code class="language-plaintext highlighter-rouge">.action</code> 和 <code class="language-plaintext highlighter-rouge">.msg</code> / <code class="language-plaintext highlighter-rouge">.srv</code> 一起列出即可：</p>

<div class="language-cmake highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">rosidl_generate_interfaces</span><span class="p">(</span><span class="si">${</span><span class="nv">PROJECT_NAME</span><span class="si">}</span>
  <span class="s2">"msg/AddressBook.msg"</span>
  <span class="s2">"srv/AddTwoInts.srv"</span>
  <span class="s2">"action/Fibonacci.action"</span>            <span class="c1"># ← 新增 action 接口</span>
<span class="p">)</span>
</code></pre></div></div>

<blockquote>
  <p>只要配置文件里同时有 <code class="language-plaintext highlighter-rouge">.msg</code> / <code class="language-plaintext highlighter-rouge">.srv</code> / <code class="language-plaintext highlighter-rouge">.action</code>，rosidl 会把 <code class="language-plaintext highlighter-rouge">Fibonacci</code> 的生成代码统一处理，生成 Python 类：<code class="language-plaintext highlighter-rouge">Fibonacci.Goal</code> / <code class="language-plaintext highlighter-rouge">Fibonacci.Result</code> / <code class="language-plaintext highlighter-rouge">Fibonacci.Feedback</code>。
（<code class="language-plaintext highlighter-rouge">rclpy</code> 里 action 相关的 <code class="language-plaintext highlighter-rouge">ActionServer</code> / <code class="language-plaintext highlighter-rouge">ActionClient</code> API 来自 <code class="language-plaintext highlighter-rouge">rclpy.action</code>。）</p>
</blockquote>

<h3 id="93-服务端-scriptsfibonacci_action_serverpy">9.3 服务端 <code class="language-plaintext highlighter-rouge">scripts/fibonacci_action_server.py</code></h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python3
</span><span class="kn">import</span> <span class="nn">rclpy</span>
<span class="kn">from</span> <span class="nn">rclpy.node</span> <span class="kn">import</span> <span class="n">Node</span>
<span class="kn">from</span> <span class="nn">rclpy.action</span> <span class="kn">import</span> <span class="n">ActionServer</span>
<span class="kn">from</span> <span class="nn">custom_interfaces.action</span> <span class="kn">import</span> <span class="n">Fibonacci</span>   <span class="c1"># 由 .action 生成
</span>

<span class="k">class</span> <span class="nc">FibonacciActionServer</span><span class="p">(</span><span class="n">Node</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="s">'fibonacci_action_server'</span><span class="p">)</span>
        <span class="c1"># create_server(动作类型, 动作名, 目标回调)
</span>        <span class="c1"># 与 srv 不同：action 用「目标回调」先接受 / 拒绝目标，
</span>        <span class="c1"># 并在执行中通过 goal_handle.publish_feedback() 回传反馈
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">_action_server</span> <span class="o">=</span> <span class="n">ActionServer</span><span class="p">(</span>
            <span class="bp">self</span><span class="p">,</span> <span class="n">Fibonacci</span><span class="p">,</span> <span class="s">'fibonacci'</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">execute_callback</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">execute_callback</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">goal_handle</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">get_logger</span><span class="p">().</span><span class="n">info</span><span class="p">(</span><span class="s">'Executing goal %d'</span> <span class="o">%</span> <span class="n">goal_handle</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">order</span><span class="p">)</span>
        <span class="n">feedback_msg</span> <span class="o">=</span> <span class="n">Fibonacci</span><span class="p">.</span><span class="n">Feedback</span><span class="p">()</span>
        <span class="n">feedback_msg</span><span class="p">.</span><span class="n">partial_sequence</span> <span class="o">=</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">]</span>

        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="n">goal_handle</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">order</span> <span class="o">+</span> <span class="mi">1</span><span class="p">):</span>
            <span class="n">feedback_msg</span><span class="p">.</span><span class="n">partial_sequence</span><span class="p">.</span><span class="n">append</span><span class="p">(</span>
                <span class="n">feedback_msg</span><span class="p">.</span><span class="n">partial_sequence</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">+</span> <span class="n">feedback_msg</span><span class="p">.</span><span class="n">partial_sequence</span><span class="p">[</span><span class="o">-</span><span class="mi">2</span><span class="p">])</span>
            <span class="n">goal_handle</span><span class="p">.</span><span class="n">publish_feedback</span><span class="p">(</span><span class="n">feedback_msg</span><span class="p">)</span>   <span class="c1"># 边算边回传反馈
</span>
        <span class="n">goal_handle</span><span class="p">.</span><span class="n">succeed</span><span class="p">()</span>                            <span class="c1"># 标记成功
</span>        <span class="n">result</span> <span class="o">=</span> <span class="n">Fibonacci</span><span class="p">.</span><span class="n">Result</span><span class="p">()</span>
        <span class="n">result</span><span class="p">.</span><span class="n">sequence</span> <span class="o">=</span> <span class="n">feedback_msg</span><span class="p">.</span><span class="n">partial_sequence</span>  <span class="c1"># 填结果
</span>        <span class="k">return</span> <span class="n">result</span>


<span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">init</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="n">args</span><span class="p">)</span>
    <span class="n">node</span> <span class="o">=</span> <span class="n">FibonacciActionServer</span><span class="p">()</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">spin</span><span class="p">(</span><span class="n">node</span><span class="p">)</span>
    <span class="n">node</span><span class="p">.</span><span class="n">destroy_node</span><span class="p">()</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">shutdown</span><span class="p">()</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="94-客户端-scriptsfibonacci_action_clientpy">9.4 客户端 <code class="language-plaintext highlighter-rouge">scripts/fibonacci_action_client.py</code></h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python3
</span><span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">import</span> <span class="nn">rclpy</span>
<span class="kn">from</span> <span class="nn">rclpy.node</span> <span class="kn">import</span> <span class="n">Node</span>
<span class="kn">from</span> <span class="nn">rclpy.action</span> <span class="kn">import</span> <span class="n">ActionClient</span>
<span class="kn">from</span> <span class="nn">custom_interfaces.action</span> <span class="kn">import</span> <span class="n">Fibonacci</span>


<span class="k">class</span> <span class="nc">FibonacciActionClient</span><span class="p">(</span><span class="n">Node</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="s">'fibonacci_action_client'</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">_action_client</span> <span class="o">=</span> <span class="n">ActionClient</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">Fibonacci</span><span class="p">,</span> <span class="s">'fibonacci'</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">send_goal</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">order</span><span class="p">):</span>
        <span class="c1"># 与服务端同理：先等服务上线，再发 goal
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">_action_client</span><span class="p">.</span><span class="n">wait_for_server</span><span class="p">()</span>
        <span class="n">goal_msg</span> <span class="o">=</span> <span class="n">Fibonacci</span><span class="p">.</span><span class="n">Goal</span><span class="p">()</span>       <span class="c1"># 填目标段字段
</span>        <span class="n">goal_msg</span><span class="p">.</span><span class="n">order</span> <span class="o">=</span> <span class="n">order</span>
        <span class="n">future</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">_action_client</span><span class="p">.</span><span class="n">send_goal_async</span><span class="p">(</span>
            <span class="n">goal_msg</span><span class="p">,</span> <span class="n">feedback_callback</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">feedback_callback</span><span class="p">)</span>   <span class="c1"># 注册反馈回调
</span>        <span class="k">return</span> <span class="n">future</span>

    <span class="k">def</span> <span class="nf">feedback_callback</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">feedback_msg</span><span class="p">):</span>
        <span class="c1"># 执行过程中实时收到反馈
</span>        <span class="n">partial</span> <span class="o">=</span> <span class="n">feedback_msg</span><span class="p">.</span><span class="n">feedback</span><span class="p">.</span><span class="n">partial_sequence</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">get_logger</span><span class="p">().</span><span class="n">info</span><span class="p">(</span><span class="s">'Feedback: %s'</span> <span class="o">%</span> <span class="n">partial</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">init</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="n">args</span><span class="p">)</span>
    <span class="n">client</span> <span class="o">=</span> <span class="n">FibonacciActionClient</span><span class="p">()</span>
    <span class="n">order</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span> <span class="k">else</span> <span class="mi">5</span>
    <span class="n">goal_future</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">send_goal</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>

    <span class="c1"># send_goal_async 先返回「接受目标」的 future，要先等它成功
</span>    <span class="n">rclpy</span><span class="p">.</span><span class="n">spin_until_future_complete</span><span class="p">(</span><span class="n">client</span><span class="p">,</span> <span class="n">goal_future</span><span class="p">)</span>
    <span class="n">goal_handle</span> <span class="o">=</span> <span class="n">goal_future</span><span class="p">.</span><span class="n">result</span><span class="p">()</span>

    <span class="k">if</span> <span class="ow">not</span> <span class="n">goal_handle</span><span class="p">.</span><span class="n">accepted</span><span class="p">:</span>
        <span class="n">client</span><span class="p">.</span><span class="n">get_logger</span><span class="p">().</span><span class="n">info</span><span class="p">(</span><span class="s">'Goal rejected'</span><span class="p">)</span>
        <span class="k">return</span>

    <span class="c1"># 目标被接受后，再等「最终结果」的 future
</span>    <span class="n">result_future</span> <span class="o">=</span> <span class="n">goal_handle</span><span class="p">.</span><span class="n">get_result_async</span><span class="p">()</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">spin_until_future_complete</span><span class="p">(</span><span class="n">client</span><span class="p">,</span> <span class="n">result_future</span><span class="p">)</span>
    <span class="n">result</span> <span class="o">=</span> <span class="n">result_future</span><span class="p">.</span><span class="n">result</span><span class="p">()</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'Result: %s'</span> <span class="o">%</span> <span class="nb">list</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">result</span><span class="p">.</span><span class="n">sequence</span><span class="p">))</span>
    <span class="n">client</span><span class="p">.</span><span class="n">destroy_node</span><span class="p">()</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">shutdown</span><span class="p">()</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<blockquote>
  <p>对比 srv 的调用套路（见 1.5 第②点）：</p>
  <ul>
    <li>srv 客户端 <code class="language-plaintext highlighter-rouge">cli.call_async(request)</code> 一步拿结果；</li>
    <li>action 客户端要<strong>分三步</strong>：<code class="language-plaintext highlighter-rouge">send_goal_async(goal, feedback_callback)</code> 先拿到「目标是否被接受」→ 被接受后再 <code class="language-plaintext highlighter-rouge">get_result_async()</code> 拿最终结果；</li>
    <li>区别的本质：服务是一次问答，动作是”接受目标 → 执行（带反馈）→ 返回结果”的完整过程。</li>
  </ul>
</blockquote>

<h3 id="95-编译运行与命令行验证">9.5 编译、运行与命令行验证</h3>

<p>在 <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> 里 <code class="language-plaintext highlighter-rouge">install(PROGRAMS ...)</code> 中把两个新脚本加进去（照抄第五节写法），然后：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/ros_ws
colcon build <span class="nt">--packages-select</span> custom_interfaces
<span class="nb">source install</span>/setup.bash
</code></pre></div></div>

<p><strong>方式 A：逐个终端手动运行</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 终端 1：动作服务端</span>
ros2 run custom_interfaces fibonacci_action_server.py

<span class="c"># 终端 2：动作客户端（算前 5 个斐波那契数）</span>
ros2 run custom_interfaces fibonacci_action_client.py 5
</code></pre></div></div>

<p><strong>方式 B：命令行直接验证（不写节点也能测）</strong></p>

<p>action 在底层会被展开为多个通信实体，可以用：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 列出与 fibonacci 相关的 topic / service</span>
ros2 action list
ros2 action info /fibonacci

<span class="c"># 查看接口定义</span>
ros2 interface show custom_interfaces/action/Fibonacci
</code></pre></div></div>

<blockquote>
  <p>底层真相（和 9.1 呼应）：
一个 <code class="language-plaintext highlighter-rouge">/fibonacci</code> action 会被 ROS 2 自动展开成 <strong>3 个 topic + 2 个 service</strong>——
3 个 topic：<code class="language-plaintext highlighter-rouge">/fibonacci/_action/goal</code>、<code class="language-plaintext highlighter-rouge">/fibonacci/_action/result</code>、<code class="language-plaintext highlighter-rouge">/fibonacci/_action/feedback</code>；
2 个 service：<code class="language-plaintext highlighter-rouge">/fibonacci/_action/cancel_goal</code>、<code class="language-plaintext highlighter-rouge">/fibonacci/_action/get_result</code>。
这正是 <code class="language-plaintext highlighter-rouge">ros2 action info /fibonacci</code> 会展示的内容。</p>
</blockquote>

<h3 id="96-三种接口的调用套路速记">9.6 三种接口的「调用套路」速记</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th><code class="language-plaintext highlighter-rouge">.msg</code></th>
      <th><code class="language-plaintext highlighter-rouge">.srv</code></th>
      <th><code class="language-plaintext highlighter-rouge">.action</code></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>一句话</td>
      <td>广播数据</td>
      <td>一问一答</td>
      <td>带进度反馈的长任务</td>
    </tr>
    <tr>
      <td>发布 / 请求端</td>
      <td><code class="language-plaintext highlighter-rouge">publish(msg)</code> 单向发</td>
      <td><code class="language-plaintext highlighter-rouge">call_async(req)</code> 一步拿结果</td>
      <td><code class="language-plaintext highlighter-rouge">send_goal_async(goal, fb_cb)</code> → <code class="language-plaintext highlighter-rouge">get_result_async()</code></td>
    </tr>
    <tr>
      <td>接收 / 服务端</td>
      <td>回调收 msg</td>
      <td>回调算完 return response</td>
      <td>接受目标 → publish_feedback 循环 → succeed + return result</td>
    </tr>
    <tr>
      <td>生成类</td>
      <td>消息类本身</td>
      <td><code class="language-plaintext highlighter-rouge">X.Request</code> / <code class="language-plaintext highlighter-rouge">X.Response</code></td>
      <td><code class="language-plaintext highlighter-rouge">X.Goal</code> / <code class="language-plaintext highlighter-rouge">X.Result</code> / <code class="language-plaintext highlighter-rouge">X.Feedback</code></td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="十使用-launch-一键弹窗运行xterm">十、使用 launch 一键弹窗运行（xterm）</h2>

<p>为了像其他示例包那样一次弹出多个 xterm 窗口分别看日志，包内提供了 <code class="language-plaintext highlighter-rouge">launch/custom_interfaces_xterm.launch.py</code>：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">launch</span> <span class="kn">import</span> <span class="n">LaunchDescription</span>
<span class="kn">from</span> <span class="nn">launch.actions</span> <span class="kn">import</span> <span class="n">ExecuteProcess</span>


<span class="k">def</span> <span class="nf">generate_launch_description</span><span class="p">():</span>
    <span class="n">env_setup</span> <span class="o">=</span> <span class="p">(</span><span class="s">'source /opt/ros/jazzy/setup.bash &amp;&amp; '</span>
                 <span class="s">'source ~/ros_ws/install/setup.bash'</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">make_terminal</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">cmd</span><span class="p">):</span>
        <span class="k">return</span> <span class="n">ExecuteProcess</span><span class="p">(</span>
            <span class="n">cmd</span><span class="o">=</span><span class="p">[</span><span class="s">'xterm'</span><span class="p">,</span> <span class="s">'-hold'</span><span class="p">,</span> <span class="s">'-T'</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="s">'-e'</span><span class="p">,</span> <span class="s">'bash'</span><span class="p">,</span> <span class="s">'-c'</span><span class="p">,</span>
                 <span class="sa">f</span><span class="s">'</span><span class="si">{</span><span class="n">env_setup</span><span class="si">}</span><span class="s"> &amp;&amp; ros2 run custom_interfaces </span><span class="si">{</span><span class="n">cmd</span><span class="si">}</span><span class="s">'</span><span class="p">],</span>
            <span class="n">output</span><span class="o">=</span><span class="s">'screen'</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">LaunchDescription</span><span class="p">([</span>
        <span class="n">make_terminal</span><span class="p">(</span><span class="s">'address_book_publisher'</span><span class="p">,</span> <span class="s">'publish_address_book.py'</span><span class="p">),</span>
        <span class="n">make_terminal</span><span class="p">(</span><span class="s">'address_book_subscriber'</span><span class="p">,</span> <span class="s">'subscribe_address_book.py'</span><span class="p">),</span>
        <span class="n">make_terminal</span><span class="p">(</span><span class="s">'add_two_ints_server'</span><span class="p">,</span> <span class="s">'add_two_ints_server.py'</span><span class="p">),</span>
        <span class="n">make_terminal</span><span class="p">(</span><span class="s">'add_two_ints_client'</span><span class="p">,</span> <span class="s">'add_two_ints_client.py 2 3'</span><span class="p">),</span>
    <span class="p">])</span>
</code></pre></div></div>

<p>一次运行四个节点，观察：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">address_book_publisher</code> 窗口：每秒 <code class="language-plaintext highlighter-rouge">Publishing Contact</code></li>
  <li><code class="language-plaintext highlighter-rouge">address_book_subscriber</code> 窗口：每秒 <code class="language-plaintext highlighter-rouge">I heard: ...</code></li>
  <li><code class="language-plaintext highlighter-rouge">add_two_ints_server</code> 窗口：<code class="language-plaintext highlighter-rouge">Incoming request a:2 b:3</code></li>
  <li><code class="language-plaintext highlighter-rouge">add_two_ints_client</code> 窗口：<code class="language-plaintext highlighter-rouge">2 + 3 = 5</code></li>
</ul>

<blockquote>
  <p>需要系统里已安装 <code class="language-plaintext highlighter-rouge">xterm</code>（<code class="language-plaintext highlighter-rouge">sudo apt install xterm</code>）；若不想开窗口，直接用第十一节的方式 A 逐个终端运行即可。</p>
</blockquote>

<hr />

<h2 id="十一编译与运行完整命令">十一、编译与运行（完整命令）</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/ros_ws
colcon build <span class="nt">--packages-select</span> custom_interfaces
<span class="nb">source install</span>/setup.bash
</code></pre></div></div>

<h3 id="方式-a逐个终端手动运行">方式 A：逐个终端手动运行</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 终端 1：消息发布者</span>
ros2 run custom_interfaces publish_address_book.py

<span class="c"># 终端 2：消息订阅者</span>
ros2 run custom_interfaces subscribe_address_book.py

<span class="c"># 终端 3：服务端</span>
ros2 run custom_interfaces add_two_ints_server.py

<span class="c"># 终端 4：客户端（发请求 2+3）</span>
ros2 run custom_interfaces add_two_ints_client.py 2 3
</code></pre></div></div>

<h3 id="方式-b一条-launch-弹-4-个-xterm">方式 B：一条 launch 弹 4 个 xterm</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ros2 launch custom_interfaces custom_interfaces_xterm.launch.py
</code></pre></div></div>

<h3 id="命令行直接验证不写节点也能测">命令行直接验证（不写节点也能测）</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 看消息</span>
ros2 topic <span class="nb">echo</span> /address_book custom_interfaces/msg/AddressBook

<span class="c"># 调服务</span>
ros2 service call /add_two_ints custom_interfaces/srv/AddTwoInts <span class="s2">"{a: 9, b: 8}"</span>
<span class="c"># 期望输出：AddTwoInts_Response(sum=17)</span>
</code></pre></div></div>

<h3 id="查看接口定义">查看接口定义</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ros2 interface show custom_interfaces/msg/AddressBook
ros2 interface show custom_interfaces/srv/AddTwoInts
</code></pre></div></div>

<hr />

<h2 id="十二msg--srv--action-三种接口对比">十二、msg / srv / action 三种接口对比</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th><code class="language-plaintext highlighter-rouge">.msg</code> 消息</th>
      <th><code class="language-plaintext highlighter-rouge">.srv</code> 服务</th>
      <th><code class="language-plaintext highlighter-rouge">.action</code> 动作</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>通信模型</td>
      <td>发布 / 订阅（单向流）</td>
      <td>请求 / 响应（一问一答）</td>
      <td>目标 / 反馈 / 结果（带过程反馈）</td>
    </tr>
    <tr>
      <td>文件分隔符</td>
      <td>无</td>
      <td>一个 <code class="language-plaintext highlighter-rouge">---</code></td>
      <td>三个 <code class="language-plaintext highlighter-rouge">---</code></td>
    </tr>
    <tr>
      <td>生成 Python 类</td>
      <td>消息类本身</td>
      <td><code class="language-plaintext highlighter-rouge">X.Request</code> + <code class="language-plaintext highlighter-rouge">X.Response</code></td>
      <td><code class="language-plaintext highlighter-rouge">X.Goal</code> / <code class="language-plaintext highlighter-rouge">X.Result</code> / <code class="language-plaintext highlighter-rouge">X.Feedback</code></td>
    </tr>
    <tr>
      <td>Python 端 API</td>
      <td><code class="language-plaintext highlighter-rouge">create_publisher</code> / <code class="language-plaintext highlighter-rouge">create_subscription</code></td>
      <td><code class="language-plaintext highlighter-rouge">create_service</code> / <code class="language-plaintext highlighter-rouge">create_client</code></td>
      <td><code class="language-plaintext highlighter-rouge">ActionServer</code> / <code class="language-plaintext highlighter-rouge">ActionClient</code></td>
    </tr>
    <tr>
      <td>典型场景</td>
      <td>传感器数据流</td>
      <td>加两个整数、查询</td>
      <td>移动到目标点（边导航边反馈）</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="十三常见坑与要点">十三、常见坑与要点</h2>

<ol>
  <li><strong>接口包必须 ament_cmake</strong>：<code class="language-plaintext highlighter-rouge">ament_python</code> 包无法定义 <code class="language-plaintext highlighter-rouge">.msg</code> / <code class="language-plaintext highlighter-rouge">.srv</code>，这是最容易犯的错。</li>
  <li><strong>搜不到话题 / 服务时试试 LOCALHOST</strong>：若发现节点之间互相发现不了，运行前执行 <code class="language-plaintext highlighter-rouge">export ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST</code>，把发现范围限定在本机。</li>
  <li><strong>命名空间跟着 CMake 的 <code class="language-plaintext highlighter-rouge">project()</code> 走</strong>：即 <code class="language-plaintext highlighter-rouge">project(custom_interfaces)</code> → 导入名是 <code class="language-plaintext highlighter-rouge">from custom_interfaces.msg import AddressBook</code>，与目录名无关。</li>
  <li><strong>msg 常量用类名访问</strong>：<code class="language-plaintext highlighter-rouge">AddressBook.PHONE_TYPE_MOBILE</code>，不是实例属性。</li>
  <li><strong>srv 请求 / 响应字段要分别访问</strong>：客户端 <code class="language-plaintext highlighter-rouge">request.a</code>，服务端 <code class="language-plaintext highlighter-rouge">response.sum</code>。</li>
  <li><strong>改接口必须重新 build</strong>：<code class="language-plaintext highlighter-rouge">.msg</code> / <code class="language-plaintext highlighter-rouge">.srv</code> 改了要 <code class="language-plaintext highlighter-rouge">colcon build</code> 才会重新生成代码。</li>
  <li><strong>脚本记得加 shebang 与可执行权限</strong>：<code class="language-plaintext highlighter-rouge">#!/usr/bin/env python3</code>（<code class="language-plaintext highlighter-rouge">install(PROGRAMS ...)</code> 已保留权限）。</li>
</ol>

<hr />

<h2 id="十四两种定义--使用接口的方案">十四、两种「定义 + 使用」接口的方案</h2>

<p>自定义接口定义好后，节点代码放在哪里用？有两条路，本教程用的是<strong>方案 B</strong>。</p>

<h3 id="方案-a应用包调用接口包接口与使用分离">方案 A：应用包调用接口包（接口与使用分离）</h3>

<p>接口归接口、节点归节点，分成两个包：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ros_ws/src/
├── custom_interfaces/       # ① 接口包（ament_cmake）：只放 .msg / .srv，不写节点
│   ├── msg/AddressBook.msg
│   ├── srv/AddTwoInts.srv
│   ├── CMakeLists.txt       # rosidl_generate_interfaces(...)
│   └── package.xml
└── custom_interfaces_app/   # ② 应用包（ament_python）：只写节点，依赖接口包
    ├── custom_interfaces_app/
    │   └── minimal_pub.py   # from custom_interfaces.msg import AddressBook
    ├── setup.py
    └── package.xml          # &lt;depend&gt;custom_interfaces&lt;/depend&gt;
</code></pre></div></div>

<p><strong>关键配置：应用包的 <code class="language-plaintext highlighter-rouge">package.xml</code> 里声明依赖</strong></p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;depend&gt;</span>custom_interfaces<span class="nt">&lt;/depend&gt;</span>   <span class="c">&lt;!-- 让应用包能找到接口包生成的类 --&gt;</span>
</code></pre></div></div>

<p>节点代码照常导入（命名空间来自接口包的工程名）：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">custom_interfaces.msg</span> <span class="kn">import</span> <span class="n">AddressBook</span>   <span class="c1"># 接口包工程名 . msg
</span><span class="kn">from</span> <span class="nn">custom_interfaces.srv</span> <span class="kn">import</span> <span class="n">AddTwoInts</span>
</code></pre></div></div>

<p><strong>构建顺序</strong>：先编译接口包，再编译应用包（实际 <code class="language-plaintext highlighter-rouge">colcon build</code> 会自动按依赖排序，一次执行即可）。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>colcon build <span class="nt">--packages-select</span> custom_interfaces custom_interfaces_app
</code></pre></div></div>

<h3 id="方案-b包内接口直接调用同包定义--同包使用">方案 B：包内接口直接调用（同包定义 + 同包使用）</h3>

<p>接口和节点放在<strong>同一个</strong> <code class="language-plaintext highlighter-rouge">ament_cmake</code> 包里（本教程 <code class="language-plaintext highlighter-rouge">custom_interfaces</code> 即此方案）：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>custom_interfaces/           # 一个 ament_cmake 包，接口 + 脚本都在里面
├── msg/  srv/  action/      # 定义接口
├── scripts/                 # 同包 Python 节点直接 import
├── CMakeLists.txt           # 生成接口 + install PROGRAMS
└── package.xml
</code></pre></div></div>

<p>节点代码里 import 自己的包：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">custom_interfaces.msg</span> <span class="kn">import</span> <span class="n">AddressBook</span>   <span class="c1"># 同包名前缀
</span></code></pre></div></div>

<h3 id="两种方案对比">两种方案对比</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>方案 A：应用包调用接口包</th>
      <th>方案 B：包内接口直接调用</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>接口定义在哪</td>
      <td>接口包（ament_cmake）</td>
      <td>同包（ament_cmake）</td>
    </tr>
    <tr>
      <td>节点写在哪</td>
      <td>应用包（ament_python）</td>
      <td>同包 <code class="language-plaintext highlighter-rouge">scripts/</code></td>
    </tr>
    <tr>
      <td>跨包依赖</td>
      <td>应用包 <code class="language-plaintext highlighter-rouge">&lt;depend&gt;</code> 接口包</td>
      <td>无</td>
    </tr>
    <tr>
      <td>构建顺序</td>
      <td>接口包先、应用包后（自动）</td>
      <td>一个包一次 build</td>
    </tr>
    <tr>
      <td>适用场景</td>
      <td>接口被多个包复用、团队共享</td>
      <td>小型示例、自用、快速验证</td>
    </tr>
  </tbody>
</table>

<p><strong>选型直觉</strong>：</p>

<ul>
  <li>接口要<strong>给很多包共用</strong>（例如全组统一的消息定义）→ 用方案 A，接口包专职定义，各业务包各写各的节点；</li>
  <li>只是<strong>自己学习 / 快速跑通一个最小案例</strong> → 用方案 B，一个包搞定，少一层依赖。</li>
</ul>

<blockquote>
  <p>两者的底层机制完全一致：接口都由 <code class="language-plaintext highlighter-rouge">rosidl_generate_interfaces</code> 生成、装进 <code class="language-plaintext highlighter-rouge">site-packages</code>，节点侧只是 <code class="language-plaintext highlighter-rouge">import</code> 路径相同、包名前缀相同。方案 A 多了一层 <code class="language-plaintext highlighter-rouge">&lt;depend&gt;</code> 依赖与构建顺序约束，换来的是接口可被多个包复用。</p>
</blockquote>

<hr />

<h2 id="十五小结">十五、小结</h2>

<ul>
  <li>三种自定义接口的定位：<strong>msg 广播数据、srv 一问一答、action 带进度反馈的长任务</strong>；</li>
  <li>接口文件分别放在包的 <code class="language-plaintext highlighter-rouge">msg/</code>、<code class="language-plaintext highlighter-rouge">srv/</code>、<code class="language-plaintext highlighter-rouge">action/</code> 目录，分隔符分别是”无 / 一行 <code class="language-plaintext highlighter-rouge">---</code> / 三个 <code class="language-plaintext highlighter-rouge">---</code>“；</li>
  <li>接口包<strong>必须是 <code class="language-plaintext highlighter-rouge">ament_cmake</code></strong>，靠 <code class="language-plaintext highlighter-rouge">rosidl_generate_interfaces()</code> 在编译期生成各语言的类；</li>
  <li>节点侧只需 <code class="language-plaintext highlighter-rouge">import</code> 生成的类，用 <code class="language-plaintext highlighter-rouge">create_publisher</code> / <code class="language-plaintext highlighter-rouge">create_subscription</code>、<code class="language-plaintext highlighter-rouge">create_service</code> / <code class="language-plaintext highlighter-rouge">create_client</code>、<code class="language-plaintext highlighter-rouge">ActionServer</code> / <code class="language-plaintext highlighter-rouge">ActionClient</code> 三套 API 即可收发；</li>
  <li>节点与接口放在同一个包里（方案 B）最省事，要复用时再拆成接口包 + 应用包（方案 A）。</li>
</ul>]]></content><author><name>老张同志</name></author><category term="ros2" /><category term="tutorial" /><summary type="html"><![CDATA[从零掌握 ROS 2 自定义接口：讲清消息（msg）、服务（srv）、动作（action）三种通信格式的区别与调用套路，用一个 ament_cmake 包同时定义 .msg / .srv / .action，并各配一个最小可运行的 Python 案例（发布订阅、服务端客户端、动作服务端客户端），覆盖建包、写接口、配 CMakeLists、构建、launch 一键弹窗运行与命令行验证，附常见坑排查表。]]></summary></entry><entry><title type="html">ROS 2 参数（Parameters）— Python 教程</title><link href="https://1q08.github.io/ros_ws/ros2/tutorial/2026/08/21/ros2-param-python.html" rel="alternate" type="text/html" title="ROS 2 参数（Parameters）— Python 教程" /><published>2026-08-21T03:49:00+00:00</published><updated>2026-08-21T03:49:00+00:00</updated><id>https://1q08.github.io/ros_ws/ros2/tutorial/2026/08/21/ros2-param-python</id><content type="html" xml:base="https://1q08.github.io/ros_ws/ros2/tutorial/2026/08/21/ros2-param-python.html"><![CDATA[<h1 id="ros-2-参数parameters-python-教程">ROS 2 参数（Parameters）— Python 教程</h1>

<blockquote>
  <p>ROS 2 中的 <strong>参数（Parameters）</strong> 是节点级的配置项，用于在<strong>不修改代码</strong>的情况下调整节点的行为，例如速度、频率、阈值、颜色等。本文带你理解参数的原理，并用 Python 从零写一个可声明、可读取、可动态修改参数的节点。</p>
</blockquote>

<hr />

<h2 id="一什么是-ros-参数">一、什么是 ROS 参数？</h2>

<h3 id="11-基本概念">1.1 基本概念</h3>

<ul>
  <li><strong>参数（Parameter）</strong>：附着在<strong>某个节点</strong>上的一个 <strong>键值对（key-value）</strong>，例如节点 <code class="language-plaintext highlighter-rouge">turtle</code> 上有参数 <code class="language-plaintext highlighter-rouge">background_r = 255</code>。</li>
  <li>每个参数都属于某个节点，通过 <strong><code class="language-plaintext highlighter-rouge">/节点名/参数名</code></strong> 来定位，例如 <code class="language-plaintext highlighter-rouge">/turtle/background_r</code>。</li>
  <li>参数由<strong>节点自己管理</strong>，可以设置默认值；运行中可以通过命令行、launch 文件或其他节点<strong>动态修改</strong>。</li>
</ul>

<pre><code class="language-mermaid">flowchart LR
    A[ros2 param set&lt;br/&gt;命令行] --&gt;|"设置 /param_node/my_int"| B[节点 param_node]
    C[launch 文件&lt;br/&gt;parameters=...] --&gt;|"启动时注入"| B
    D[其他节点&lt;br/&gt;set_parameters] --&gt;|"运行时修改"| B
    B --&gt;|"get_parameter&lt;br/&gt;读取"| E[节点逻辑&lt;br/&gt;使用参数值]
</code></pre>

<h3 id="12-参数-vs-话题topic">1.2 参数 vs 话题（Topic）</h3>

<table>
  <thead>
    <tr>
      <th>对比项</th>
      <th>参数（Parameter）</th>
      <th>话题（Topic）</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>归属</strong></td>
      <td>属于某个节点</td>
      <td>不属于任何节点，全局广播</td>
    </tr>
    <tr>
      <td><strong>内容</strong></td>
      <td>单个键值对（配置项）</td>
      <td>结构化的消息流</td>
    </tr>
    <tr>
      <td><strong>方向</strong></td>
      <td>无方向，可读可写</td>
      <td>单向：发布者 → 订阅者</td>
    </tr>
    <tr>
      <td><strong>用途</strong></td>
      <td>配置 / 调参</td>
      <td>数据传输 / 通信</td>
    </tr>
    <tr>
      <td><strong>数据量</strong></td>
      <td>小，偶发改变</td>
      <td>可高频、大量</td>
    </tr>
    <tr>
      <td><strong>修改方式</strong></td>
      <td>一次性设置即可</td>
      <td>持续发布</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>打个比方：<strong>参数像汽车仪表盘上的旋钮</strong>（转速、亮度、音量……），调一次管用；<strong>话题像电台广播</strong>（数据流），一直在播。调参用参数，通信用话题，二者是 ROS 2 中相辅相成的两种机制。</p>
</blockquote>

<h3 id="13-参数的数据类型">1.3 参数的数据类型</h3>

<table>
  <thead>
    <tr>
      <th>类型</th>
      <th>说明</th>
      <th>示例</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">integer</code></td>
      <td>整数</td>
      <td><code class="language-plaintext highlighter-rouge">42</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">double</code></td>
      <td>浮点数</td>
      <td><code class="language-plaintext highlighter-rouge">3.14</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">string</code></td>
      <td>字符串</td>
      <td><code class="language-plaintext highlighter-rouge">"hello"</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bool</code></td>
      <td>布尔值</td>
      <td><code class="language-plaintext highlighter-rouge">true</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">integer_array</code></td>
      <td>整型数组</td>
      <td><code class="language-plaintext highlighter-rouge">[1, 2, 3]</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">double_array</code></td>
      <td>浮点数组</td>
      <td><code class="language-plaintext highlighter-rouge">[1.5, 2.5]</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">string_array</code></td>
      <td>字符串数组</td>
      <td><code class="language-plaintext highlighter-rouge">["a", "b"]</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bool_array</code></td>
      <td>布尔数组</td>
      <td><code class="language-plaintext highlighter-rouge">[true, false]</code></td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>还有 <code class="language-plaintext highlighter-rouge">byte[]</code>（字节数组）等，但上面这 8 种是最常用的。一个参数在任意时刻<strong>只能取一种类型</strong>。</p>
</blockquote>

<hr />

<h2 id="二准备工作">二、准备工作</h2>

<p>本文基于 <strong>ROS 2 Jazzy + Python 3</strong>，假设你的环境已经配置好：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 检查 ROS 2 是否可用</span>
<span class="nb">printenv </span>ROS_DISTRO        <span class="c"># 应输出 jazzy</span>

<span class="c"># 每次打开终端都要 source 环境（也可写入 ~/.bashrc）</span>
<span class="nb">source</span> /opt/ros/jazzy/setup.bash
<span class="nb">source</span> ~/ros_ws/install/setup.bash
</code></pre></div></div>

<hr />

<h2 id="三最小代码样例">三、最小代码样例</h2>

<p>下面是一个使用参数的完整节点：它声明了 6 个参数，每 0.5 秒读取并打印一次。</p>

<h3 id="31-节点-param_nodepy">3.1 节点 <code class="language-plaintext highlighter-rouge">param_node.py</code></h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">rclpy</span>
<span class="kn">from</span> <span class="nn">rclpy.node</span> <span class="kn">import</span> <span class="n">Node</span>


<span class="k">class</span> <span class="nc">ParamNode</span><span class="p">(</span><span class="n">Node</span><span class="p">):</span>
    <span class="s">"""演示 ROS 2 参数的节点：声明参数 → 定时读取并打印。"""</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="s">'param_node'</span><span class="p">)</span>

        <span class="c1"># ---- 1. 声明参数（名字 + 默认值）----
</span>        <span class="c1">#    声明之后才能被 ros2 param list 看到、被命令行/launch 修改
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">declare_parameter</span><span class="p">(</span><span class="s">'my_str'</span><span class="p">,</span> <span class="s">'world'</span><span class="p">)</span>      <span class="c1"># 字符串参数，默认 'world'
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">declare_parameter</span><span class="p">(</span><span class="s">'my_int'</span><span class="p">,</span> <span class="mi">42</span><span class="p">)</span>           <span class="c1"># 整数参数，默认 42
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">declare_parameter</span><span class="p">(</span><span class="s">'my_double'</span><span class="p">,</span> <span class="mf">3.14</span><span class="p">)</span>      <span class="c1"># 浮点参数，默认 3.14
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">declare_parameter</span><span class="p">(</span><span class="s">'my_bool'</span><span class="p">,</span> <span class="bp">True</span><span class="p">)</span>        <span class="c1"># 布尔参数，默认 True
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">declare_parameter</span><span class="p">(</span><span class="s">'my_array'</span><span class="p">,</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">])</span>  <span class="c1"># 整型数组参数，默认 [1,2,3]
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">declare_parameter</span><span class="p">(</span><span class="s">'my_enum'</span><span class="p">,</span> <span class="s">'A'</span><span class="p">)</span>         <span class="c1"># 模拟枚举：只允许 A/B/C
</span>
        <span class="c1"># ---- 2. 定时器：每 0.5 秒读取并打印一次参数 ----
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">timer</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">create_timer</span><span class="p">(</span><span class="mf">0.5</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">timer_callback</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">timer_callback</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="c1"># get_parameter() 返回 Parameter 对象，用 .value 取实际值
</span>        <span class="n">s</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">get_parameter</span><span class="p">(</span><span class="s">'my_str'</span><span class="p">).</span><span class="n">value</span>
        <span class="n">i</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">get_parameter</span><span class="p">(</span><span class="s">'my_int'</span><span class="p">).</span><span class="n">value</span>
        <span class="n">d</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">get_parameter</span><span class="p">(</span><span class="s">'my_double'</span><span class="p">).</span><span class="n">value</span>
        <span class="n">b</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">get_parameter</span><span class="p">(</span><span class="s">'my_bool'</span><span class="p">).</span><span class="n">value</span>
        <span class="n">arr</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">get_parameter</span><span class="p">(</span><span class="s">'my_array'</span><span class="p">).</span><span class="n">value</span>
        <span class="n">e</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">get_parameter</span><span class="p">(</span><span class="s">'my_enum'</span><span class="p">).</span><span class="n">value</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">get_logger</span><span class="p">().</span><span class="n">info</span><span class="p">(</span>
            <span class="sa">f</span><span class="s">'my_str=</span><span class="si">{</span><span class="n">s</span><span class="si">}</span><span class="s"> my_int=</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s"> my_double=</span><span class="si">{</span><span class="n">d</span><span class="si">}</span><span class="s"> '</span>
            <span class="sa">f</span><span class="s">'my_bool=</span><span class="si">{</span><span class="n">b</span><span class="si">}</span><span class="s"> my_array=</span><span class="si">{</span><span class="n">arr</span><span class="si">}</span><span class="s"> my_enum=</span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">init</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="n">args</span><span class="p">)</span>        <span class="c1"># 1. 初始化 rclpy
</span>    <span class="n">node</span> <span class="o">=</span> <span class="n">ParamNode</span><span class="p">()</span>           <span class="c1"># 2. 实例化节点（此时会声明参数）
</span>    <span class="k">try</span><span class="p">:</span>
        <span class="n">rclpy</span><span class="p">.</span><span class="n">spin</span><span class="p">(</span><span class="n">node</span><span class="p">)</span>         <span class="c1"># 3. 阻塞运行，持续处理回调
</span>    <span class="k">except</span> <span class="nb">KeyboardInterrupt</span><span class="p">:</span>
        <span class="k">pass</span>                     <span class="c1"># 按 Ctrl+C 静默退出
</span>    <span class="k">finally</span><span class="p">:</span>
        <span class="n">node</span><span class="p">.</span><span class="n">destroy_node</span><span class="p">()</span>      <span class="c1"># 4. 清理
</span>        <span class="n">rclpy</span><span class="p">.</span><span class="n">shutdown</span><span class="p">()</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="32-代码要点解读">3.2 代码要点解读</h3>

<table>
  <thead>
    <tr>
      <th>代码</th>
      <th>作用</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">declare_parameter(名字, 默认值)</code></td>
      <td><strong>声明</strong>一个参数并给定默认值。未声明的参数在读取时会有警告</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">get_parameter(名字)</code></td>
      <td>读取参数，返回一个 <code class="language-plaintext highlighter-rouge">Parameter</code> 对象</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">.value</code></td>
      <td>取出 <code class="language-plaintext highlighter-rouge">Parameter</code> 对象里的实际值（int / float / str / bool / list）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">create_timer(秒, 回调)</code></td>
      <td>定时触发回调，这里用来周期性打印当前参数值</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>为什么要 <code class="language-plaintext highlighter-rouge">declare_parameter</code>？</strong>
声明参数有两个好处：一是参数有了<strong>默认值</strong>（不传也能跑）；二是参数会出现在 <code class="language-plaintext highlighter-rouge">ros2 param list</code> 中，并且能被 <code class="language-plaintext highlighter-rouge">ros2 param set</code>、launch 文件合法地修改。不声明直接 <code class="language-plaintext highlighter-rouge">get_parameter</code> 会得到”未声明”的警告。</p>
</blockquote>

<hr />

<h2 id="四进阶动态修改参数--合法性校验">四、进阶：动态修改参数 + 合法性校验</h2>

<p>默认情况下，<code class="language-plaintext highlighter-rouge">ros2 param set</code> 可以随意改参数值。如果想在<strong>修改时拦截并校验</strong>（比如枚举值只能取 A/B/C），可以用 <code class="language-plaintext highlighter-rouge">add_on_set_parameters_callback</code> 注册回调。</p>

<h3 id="41-动态参数节点-param_node_dynamicpy">4.1 动态参数节点 <code class="language-plaintext highlighter-rouge">param_node_dynamic.py</code></h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">rclpy</span>
<span class="kn">from</span> <span class="nn">rclpy.node</span> <span class="kn">import</span> <span class="n">Node</span>
<span class="kn">from</span> <span class="nn">rclpy.parameter</span> <span class="kn">import</span> <span class="n">Parameter</span>
<span class="kn">from</span> <span class="nn">rclpy.parameters</span> <span class="kn">import</span> <span class="n">SetParametersResult</span>


<span class="k">class</span> <span class="nc">DynamicParamNode</span><span class="p">(</span><span class="n">Node</span><span class="p">):</span>
    <span class="s">"""演示动态参数修改：允许 set 的同时做合法性校验。"""</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="s">'param_node_dynamic'</span><span class="p">)</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">declare_parameter</span><span class="p">(</span><span class="s">'my_enum'</span><span class="p">,</span> <span class="s">'A'</span><span class="p">)</span>   <span class="c1"># 只允许 A/B/C
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">declare_parameter</span><span class="p">(</span><span class="s">'my_int'</span><span class="p">,</span> <span class="mi">42</span><span class="p">)</span>     <span class="c1"># 只允许 0~100
</span>
        <span class="c1"># 注册"参数被修改时"的回调
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">add_on_set_parameters_callback</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">param_callback</span><span class="p">)</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">timer</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">create_timer</span><span class="p">(</span><span class="mf">1.0</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">timer_callback</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">param_callback</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">params</span><span class="p">):</span>
        <span class="s">"""每次参数被 set 时调用。返回 SetParametersResult 表示接受/拒绝。"""</span>
        <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">params</span><span class="p">:</span>                     <span class="c1"># params 是一批 Parameter 对象
</span>            <span class="k">if</span> <span class="n">p</span><span class="p">.</span><span class="n">name</span> <span class="o">==</span> <span class="s">'my_enum'</span> <span class="ow">and</span> <span class="n">p</span><span class="p">.</span><span class="n">value</span> <span class="ow">not</span> <span class="ow">in</span> <span class="p">(</span><span class="s">'A'</span><span class="p">,</span> <span class="s">'B'</span><span class="p">,</span> <span class="s">'C'</span><span class="p">):</span>
                <span class="k">return</span> <span class="n">SetParametersResult</span><span class="p">(</span>
                    <span class="n">successful</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">reason</span><span class="o">=</span><span class="s">'my_enum 只能是 A/B/C'</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">p</span><span class="p">.</span><span class="n">name</span> <span class="o">==</span> <span class="s">'my_int'</span> <span class="ow">and</span> <span class="ow">not</span> <span class="p">(</span><span class="mi">0</span> <span class="o">&lt;=</span> <span class="n">p</span><span class="p">.</span><span class="n">value</span> <span class="o">&lt;=</span> <span class="mi">100</span><span class="p">):</span>
                <span class="k">return</span> <span class="n">SetParametersResult</span><span class="p">(</span>
                    <span class="n">successful</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">reason</span><span class="o">=</span><span class="s">'my_int 必须在 0~100 之间'</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">SetParametersResult</span><span class="p">(</span><span class="n">successful</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">timer_callback</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">get_logger</span><span class="p">().</span><span class="n">info</span><span class="p">(</span>
            <span class="sa">f</span><span class="s">'my_enum=</span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="n">get_parameter</span><span class="p">(</span><span class="s">"my_enum"</span><span class="p">).</span><span class="n">value</span><span class="si">}</span><span class="s"> '</span>
            <span class="sa">f</span><span class="s">'my_int=</span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="n">get_parameter</span><span class="p">(</span><span class="s">"my_int"</span><span class="p">).</span><span class="n">value</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">init</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="n">args</span><span class="p">)</span>
    <span class="n">node</span> <span class="o">=</span> <span class="n">DynamicParamNode</span><span class="p">()</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">rclpy</span><span class="p">.</span><span class="n">spin</span><span class="p">(</span><span class="n">node</span><span class="p">)</span>
    <span class="k">except</span> <span class="nb">KeyboardInterrupt</span><span class="p">:</span>
        <span class="k">pass</span>
    <span class="k">finally</span><span class="p">:</span>
        <span class="n">node</span><span class="p">.</span><span class="n">destroy_node</span><span class="p">()</span>
        <span class="n">rclpy</span><span class="p">.</span><span class="n">shutdown</span><span class="p">()</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="42-校验回调的返回类型">4.2 校验回调的返回类型</h3>

<table>
  <thead>
    <tr>
      <th>返回</th>
      <th>含义</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SetParametersResult(successful=True)</code></td>
      <td>接受本次修改</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SetParametersResult(successful=False, reason='...')</code></td>
      <td>拒绝本次修改，<code class="language-plaintext highlighter-rouge">reason</code> 会反馈给调用方</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>注意：校验回调的参数 <code class="language-plaintext highlighter-rouge">params</code> 是一个<strong>列表</strong>（一次可能同时改多个参数），所以用 <code class="language-plaintext highlighter-rouge">for p in params</code> 逐个检查。只要有一个不合法，就返回 <code class="language-plaintext highlighter-rouge">successful=False</code>，整批修改都会被拒绝。</p>
</blockquote>

<hr />

<h2 id="五完整过程从包到运行">五、完整过程（从包到运行）</h2>

<h3 id="第-1-步创建功能包">第 1 步：创建功能包</h3>

<p>在 <code class="language-plaintext highlighter-rouge">src/</code> 下用官方命令创建 Python 包（这里包名用 <code class="language-plaintext highlighter-rouge">py_param</code> 演示，也可换成你自己的名字）：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/ros_ws/src
ros2 pkg create py_param <span class="nt">--build-type</span> ament_python <span class="nt">--node-name</span> param_node
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--node-name param_node</code> 会自动生成 <code class="language-plaintext highlighter-rouge">py_param/param_node.py</code> 并在 <code class="language-plaintext highlighter-rouge">setup.py</code> 中注册入口。</p>

<h3 id="第-2-步放置代码文件">第 2 步：放置代码文件</h3>

<p>把上面的代码保存为：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src/py_param/py_param/param_node.py          # 基础版：声明 + 读取
src/py_param/py_param/param_node_dynamic.py  # 进阶版：动态修改 + 校验
</code></pre></div></div>

<h3 id="第-3-步配置-setuppy">第 3 步：配置 <code class="language-plaintext highlighter-rouge">setup.py</code></h3>

<p>编辑 <code class="language-plaintext highlighter-rouge">src/py_param/setup.py</code>，在 <code class="language-plaintext highlighter-rouge">console_scripts</code> 中注册两个可执行入口：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">entry_points</span><span class="o">=</span><span class="p">{</span>
    <span class="s">'console_scripts'</span><span class="p">:</span> <span class="p">[</span>
        <span class="s">'param_node = py_param.param_node:main'</span><span class="p">,</span>
        <span class="s">'param_node_dynamic = py_param.param_node_dynamic:main'</span><span class="p">,</span>
    <span class="p">],</span>
<span class="p">},</span>
</code></pre></div></div>

<h3 id="第-4-步确认-packagexml-依赖">第 4 步：确认 <code class="language-plaintext highlighter-rouge">package.xml</code> 依赖</h3>

<p>确保 <code class="language-plaintext highlighter-rouge">package.xml</code> 里声明了 <code class="language-plaintext highlighter-rouge">rclpy</code>（<code class="language-plaintext highlighter-rouge">ros2 pkg create</code> 默认已带）：</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;exec_depend&gt;</span>rclpy<span class="nt">&lt;/exec_depend&gt;</span>
</code></pre></div></div>

<h3 id="第-5-步构建">第 5 步：构建</h3>

<p>回到工作区根目录，构建这个包（<strong>每次改代码后都要重新构建</strong>）：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/ros_ws
colcon build <span class="nt">--packages-select</span> py_param
<span class="nb">source install</span>/setup.bash
</code></pre></div></div>

<h3 id="第-6-步运行与命令行调参">第 6 步：运行与命令行调参</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 终端 1：启动节点</span>
<span class="nb">source</span> /opt/ros/jazzy/setup.bash
<span class="nb">source</span> ~/ros_ws/install/setup.bash
ros2 run py_param param_node
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 终端 2：查看 / 修改参数</span>
<span class="nb">source</span> /opt/ros/jazzy/setup.bash
<span class="nb">source</span> ~/ros_ws/install/setup.bash

ros2 param list /param_node           <span class="c"># 列出该节点的所有参数</span>
ros2 param get /param_node my_str     <span class="c"># 获取单个参数值</span>
ros2 param <span class="nb">set</span> /param_node my_int 100 <span class="c"># 动态修改参数（终端 1 会立即打印新值）</span>
</code></pre></div></div>

<h3 id="第-7-步验证结果">第 7 步：验证结果</h3>

<ul>
  <li><strong>终端 1</strong> 会周期性打印：<code class="language-plaintext highlighter-rouge">my_str=world my_int=42 ...</code></li>
  <li>在<strong>终端 2</strong> 执行 <code class="language-plaintext highlighter-rouge">ros2 param set /param_node my_int 100</code> 后，终端 1 立刻变为 <code class="language-plaintext highlighter-rouge">my_int=100</code></li>
  <li>试试动态版本校验：</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ros2 run py_param param_node_dynamic
<span class="c"># 另一个终端：</span>
ros2 param <span class="nb">set</span> /param_node_dynamic my_enum X   <span class="c"># 会被拒绝：my_enum 只能是 A/B/C</span>
ros2 param <span class="nb">set</span> /param_node_dynamic my_enum B   <span class="c"># 成功</span>
ros2 param <span class="nb">set</span> /param_node_dynamic my_int 999  <span class="c"># 会被拒绝：必须在 0~100 之间</span>
</code></pre></div></div>

<p>预期输出示例：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ros2 param set /param_node_dynamic my_enum X
Setting parameter failed: my_enum 只能是 A/B/C
$ ros2 param set /param_node_dynamic my_enum B
Set parameter successful
</code></pre></div></div>

<hr />

<h2 id="六在-launch-文件中使用参数">六、在 launch 文件中使用参数</h2>

<p>参数也可以在<strong>启动时</strong>通过 launch 文件注入，这样”改参数不用动代码”。</p>

<h3 id="61-直接传参">6.1 直接传参</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">launch</span> <span class="kn">import</span> <span class="n">LaunchDescription</span>
<span class="kn">from</span> <span class="nn">launch_ros.actions</span> <span class="kn">import</span> <span class="n">Node</span>

<span class="k">def</span> <span class="nf">generate_launch_description</span><span class="p">():</span>
    <span class="k">return</span> <span class="n">LaunchDescription</span><span class="p">([</span>
        <span class="n">Node</span><span class="p">(</span>
            <span class="n">package</span><span class="o">=</span><span class="s">'py_param'</span><span class="p">,</span>
            <span class="n">executable</span><span class="o">=</span><span class="s">'param_node'</span><span class="p">,</span>
            <span class="c1"># 启动时把参数注入到节点，等价于逐个 --ros-args -p
</span>            <span class="n">parameters</span><span class="o">=</span><span class="p">[{</span>
                <span class="s">'my_str'</span><span class="p">:</span> <span class="s">'hello launch'</span><span class="p">,</span>
                <span class="s">'my_int'</span><span class="p">:</span> <span class="mi">99</span><span class="p">,</span>
                <span class="s">'my_enum'</span><span class="p">:</span> <span class="s">'B'</span><span class="p">,</span>
            <span class="p">}],</span>
        <span class="p">)</span>
    <span class="p">])</span>
</code></pre></div></div>

<h3 id="62-从-yaml-文件加载配合-ros2-param-dump">6.2 从 YAML 文件加载（配合 <code class="language-plaintext highlighter-rouge">ros2 param dump</code>）</h3>

<p>先导出节点当前参数到 YAML：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ros2 param dump /param_node            <span class="c"># 生成 param_node.yaml</span>
ros2 param dump /param_node <span class="nt">--output-dir</span> ~/ros_ws/src/py_param/config/
</code></pre></div></div>

<p>然后在 launch 中加载该文件：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">launch</span> <span class="kn">import</span> <span class="n">LaunchDescription</span>
<span class="kn">from</span> <span class="nn">launch.actions</span> <span class="kn">import</span> <span class="n">DeclareLaunchArgument</span>
<span class="kn">from</span> <span class="nn">launch.substitutions</span> <span class="kn">import</span> <span class="n">PathJoinSubstitution</span><span class="p">,</span> <span class="n">LaunchConfiguration</span>
<span class="kn">from</span> <span class="nn">launch_ros.actions</span> <span class="kn">import</span> <span class="n">Node</span>
<span class="kn">from</span> <span class="nn">launch_ros.parameter_descriptions</span> <span class="kn">import</span> <span class="n">ParameterFile</span>

<span class="k">def</span> <span class="nf">generate_launch_description</span><span class="p">():</span>
    <span class="c1"># 假设 config 目录在功能包内，路径会被正确解析
</span>    <span class="n">param_file</span> <span class="o">=</span> <span class="n">PathJoinSubstitution</span><span class="p">([</span>
        <span class="n">LaunchConfiguration</span><span class="p">(</span><span class="s">'param_file'</span><span class="p">),</span>
    <span class="p">])</span>

    <span class="k">return</span> <span class="n">LaunchDescription</span><span class="p">([</span>
        <span class="n">DeclareLaunchArgument</span><span class="p">(</span>
            <span class="s">'param_file'</span><span class="p">,</span>
            <span class="n">default_value</span><span class="o">=</span><span class="s">'src/py_param/config/param_node.yaml'</span><span class="p">,</span>
            <span class="n">description</span><span class="o">=</span><span class="s">'参数文件路径'</span><span class="p">),</span>
        <span class="n">Node</span><span class="p">(</span>
            <span class="n">package</span><span class="o">=</span><span class="s">'py_param'</span><span class="p">,</span>
            <span class="n">executable</span><span class="o">=</span><span class="s">'param_node'</span><span class="p">,</span>
            <span class="n">parameters</span><span class="o">=</span><span class="p">[</span><span class="n">ParameterFile</span><span class="p">(</span><span class="n">param_file</span><span class="p">,</span> <span class="n">allow_substs</span><span class="o">=</span><span class="bp">True</span><span class="p">)],</span>
        <span class="p">),</span>
    <span class="p">])</span>
</code></pre></div></div>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">ros2 param dump</code> 生成的 YAML 会包含 <code class="language-plaintext highlighter-rouge">/**</code> 前缀（表示”任意命名空间下的该节点”），launch 加载时会自动套用到本节点的命名空间。</p>
</blockquote>

<hr />

<h2 id="七常用命令行速查">七、常用命令行速查</h2>

<table>
  <thead>
    <tr>
      <th>命令</th>
      <th>作用</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ros2 param list /节点名</code></td>
      <td>列出节点的所有参数</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ros2 param get /节点名 参数名</code></td>
      <td>获取某个参数值</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ros2 param set /节点名 参数名 值</code></td>
      <td>设置某个参数值（可触发校验回调）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ros2 param describe /节点名 参数名</code></td>
      <td>查看参数类型、默认值、描述</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ros2 param dump /节点名</code></td>
      <td>把节点参数导出为 YAML 文件</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ros2 param load /节点名 文件.yaml</code></td>
      <td>从 YAML 文件批量加载参数</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ros2 run 包 可执行 --ros-args -p 名:=值</code></td>
      <td>启动时直接传参</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="八常见问题排查">八、常见问题排查</h2>

<table>
  <thead>
    <tr>
      <th>现象</th>
      <th>原因 / 解决</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ros2 param list</code> 看不到自己声明的参数</td>
      <td>忘记 <code class="language-plaintext highlighter-rouge">declare_parameter</code>，只声明了却没调用；或节点没在运行</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">get_parameter</code> 有 “undeclared parameter” 警告</td>
      <td>参数未声明就读取，先用 <code class="language-plaintext highlighter-rouge">declare_parameter</code> 声明</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ros2 param set</code> 提示找不到节点</td>
      <td>参数是<strong>属于节点的</strong>，先确认节点在运行：<code class="language-plaintext highlighter-rouge">ros2 node list</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ros2 param set</code> 修改无效</td>
      <td>节点用 <code class="language-plaintext highlighter-rouge">add_on_set_parameters_callback</code> 拒绝了，或节点没读取该参数</td>
    </tr>
    <tr>
      <td>launch 里传的参数不生效</td>
      <td>参数名写错，或 <code class="language-plaintext highlighter-rouge">parameters</code> 字典键名与 <code class="language-plaintext highlighter-rouge">declare_parameter</code> 的名字不一致</td>
    </tr>
    <tr>
      <td>改了代码但行为没变</td>
      <td>忘记重新构建：<code class="language-plaintext highlighter-rouge">colcon build --packages-select py_param</code></td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="九小结">九、小结</h2>

<ul>
  <li><strong>参数</strong>是节点级的<strong>键值对配置</strong>，用于在不改代码的情况下调整节点行为。</li>
  <li>用 Python 只需掌握四个关键点：<code class="language-plaintext highlighter-rouge">declare_parameter</code>（声明）、<code class="language-plaintext highlighter-rouge">get_parameter(...).value</code>（读取）、<code class="language-plaintext highlighter-rouge">add_on_set_parameters_callback</code>（修改时校验）、<code class="language-plaintext highlighter-rouge">ros2 param set</code>（命令行调参）。</li>
  <li>参数可以从三个入口设置：<strong>代码默认值</strong> → <strong>launch 文件</strong> → <strong>命令行/其他节点</strong>，后设置的会覆盖先前的。</li>
  <li>完整流程：<strong>建包 → 声明参数 → 注册入口 → 构建 → 运行 → 调参</strong>。</li>
</ul>

<p>掌握了参数，你就有了给节点”拧旋钮”的能力。结合之前学的<strong>话题（Topic）</strong>和后面的<strong>服务（Service）</strong>，就能搭建出灵活、可配置的机器人系统。</p>]]></content><author><name>老张同志</name></author><category term="ros2" /><category term="tutorial" /><summary type="html"><![CDATA[从零掌握 ROS 2 参数（Parameters）：理解节点级键值对配置的原理与数据类型，用 Python 手写可声明、可读取、可动态修改（含合法性校验）的参数节点，覆盖建包、构建、命令行调参、launch 文件注入参数，附常用命令速查表与排查表。]]></summary></entry><entry><title type="html">ROS 2 服务端（Service Server）与客户端（Service Client）</title><link href="https://1q08.github.io/ros_ws/ros2/tutorial/2026/08/21/ros2-service-python.html" rel="alternate" type="text/html" title="ROS 2 服务端（Service Server）与客户端（Service Client）" /><published>2026-08-21T03:42:00+00:00</published><updated>2026-08-21T03:42:00+00:00</updated><id>https://1q08.github.io/ros_ws/ros2/tutorial/2026/08/21/ros2-service-python</id><content type="html" xml:base="https://1q08.github.io/ros_ws/ros2/tutorial/2026/08/21/ros2-service-python.html"><![CDATA[<h1 id="ros-2-服务端service-server与客户端service-client">ROS 2 服务端（Service Server）与客户端（Service Client）</h1>

<blockquote>
  <p>ROS 2 中节点间通信除了异步的 <strong>话题（Topic）</strong> 模式，还有一种同步的 <strong>服务 / 客户端（Service）</strong> 模式。本文带你理解它的原理，并用 Python 从零写出一个最小可运行的服务端与客户端。</p>
</blockquote>

<hr />

<h2 id="一什么是服务端与客户端">一、什么是服务端与客户端？</h2>

<p>在 ROS 2 中，<strong>话题</strong>适合”一个发、多个收”的<strong>异步</strong>数据流；而<strong>服务</strong>适合”请求一次、得到一次结果”的<strong>同步</strong>问答式通信：</p>

<ul>
  <li><strong>服务端（Service Server）</strong>：接收客户端的请求，处理并返回响应。</li>
  <li><strong>客户端（Service Client）</strong>：发起请求，然后<strong>阻塞等待</strong>服务端的响应。</li>
</ul>

<pre><code class="language-mermaid">sequenceDiagram
    participant C as 客户端节点&lt;br/&gt;minimal_client
    participant S as 服务端节点&lt;br/&gt;minimal_service
    C-&gt;&gt;S: 请求 request (a=41, b=1)
    Note over S: 处理请求&lt;br/&gt;sum = a + b
    S--&gt;&gt;C: 响应 response (sum=42)
</code></pre>

<p>这种模式的几个关键特点：</p>

<table>
  <thead>
    <tr>
      <th>特点</th>
      <th>说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>同步</strong></td>
      <td>客户端发出请求后要<strong>等待</strong>服务端返回结果，是”一问一答”</td>
    </tr>
    <tr>
      <td><strong>一对一</strong></td>
      <td>一次请求对应一次响应，且同一时刻只有一个客户端能调用某个服务</td>
    </tr>
    <tr>
      <td><strong>有始有终</strong></td>
      <td>请求与响应各有一个类型定义（<code class="language-plaintext highlighter-rouge">Request</code> 与 <code class="language-plaintext highlighter-rouge">Response</code>），成对出现</td>
    </tr>
    <tr>
      <td><strong>面向调用</strong></td>
      <td>适合”查询状态、触发动作并拿结果”的场景，如让机器人做个动作、读取传感器数值</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>打个比方：<strong>服务就像餐厅点餐</strong>。你（客户端）叫来服务员下单（请求），服务员把菜做好端上来（响应），你拿到菜后才离开。而话题则像广播电台，电台只管播，听众随时听，双方互不等待。</p>
</blockquote>

<blockquote>
  <p>何时用话题、何时用服务？<strong>持续流动的数据</strong>（传感器流、状态流）用话题；<strong>“调用一次、等待结果”</strong>（一次性查询、一次性指令）用服务。</p>
</blockquote>

<hr />

<h2 id="二准备工作">二、准备工作</h2>

<p>本文基于 <strong>ROS 2 Jazzy + Python 3</strong>，假设你的环境已经配置好：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 检查 ROS 2 是否可用</span>
<span class="nb">printenv </span>ROS_DISTRO        <span class="c"># 应输出 jazzy</span>

<span class="c"># 每次打开终端都要 source 环境（也可写入 ~/.bashrc）</span>
<span class="nb">source</span> /opt/ros/jazzy/setup.bash
<span class="nb">source</span> ~/ros_ws/install/setup.bash
</code></pre></div></div>

<p>本文使用官方标准服务接口 <code class="language-plaintext highlighter-rouge">example_interfaces/srv/AddTwoInts</code>（接收两个整数，返回它们的和），无需自定义接口，是最省事的入门方式。</p>

<hr />

<h2 id="三最小代码样例">三、最小代码样例</h2>

<p>下面是最精简的服务端 / 客户端，使用标准接口 <code class="language-plaintext highlighter-rouge">example_interfaces/srv/AddTwoInts</code>，服务名为 <code class="language-plaintext highlighter-rouge">add_two_ints</code>。</p>

<h3 id="31-服务端-minimal_servicepy">3.1 服务端 <code class="language-plaintext highlighter-rouge">minimal_service.py</code></h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">rclpy</span>                    <span class="c1"># ROS 2 Python 客户端库
</span><span class="kn">from</span> <span class="nn">rclpy.node</span> <span class="kn">import</span> <span class="n">Node</span>     <span class="c1"># 节点基类
</span><span class="kn">from</span> <span class="nn">example_interfaces.srv</span> <span class="kn">import</span> <span class="n">AddTwoInts</span>  <span class="c1"># 服务接口类型（请求/响应的数据结构）
</span>

<span class="k">class</span> <span class="nc">MinimalService</span><span class="p">(</span><span class="n">Node</span><span class="p">):</span>
    <span class="s">"""服务端节点：提供 add_two_ints 服务，接收两个整数，返回它们的和"""</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="s">'minimal_service'</span><span class="p">)</span>          <span class="c1"># 节点名称（ros2 node list 可见）
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">srv</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">create_service</span><span class="p">(</span>              <span class="c1"># 创建服务端
</span>            <span class="n">AddTwoInts</span><span class="p">,</span>               <span class="c1"># 服务接口类型（决定请求/响应各有哪些字段）
</span>            <span class="s">'add_two_ints'</span><span class="p">,</span>           <span class="c1"># 服务名称（客户端必须用同名调用）
</span>            <span class="bp">self</span><span class="p">.</span><span class="n">add_two_ints_callback</span><span class="p">)</span>  <span class="c1"># 收到请求时调用的回调函数
</span>
    <span class="k">def</span> <span class="nf">add_two_ints_callback</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="n">response</span><span class="p">):</span>
        <span class="s">"""处理请求并返回响应。
        request  是客户端发来的数据（字段 a、b）
        response 是要回传给客户端的结果（字段 sum）"""</span>
        <span class="n">response</span><span class="p">.</span><span class="nb">sum</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">a</span> <span class="o">+</span> <span class="n">request</span><span class="p">.</span><span class="n">b</span>          <span class="c1"># 取两个整数相加，填入响应字段
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">get_logger</span><span class="p">().</span><span class="n">info</span><span class="p">(</span>
            <span class="sa">f</span><span class="s">'Incoming request: a=</span><span class="si">{</span><span class="n">request</span><span class="p">.</span><span class="n">a</span><span class="si">}</span><span class="s">, b=</span><span class="si">{</span><span class="n">request</span><span class="p">.</span><span class="n">b</span><span class="si">}</span><span class="s"> -&gt; sum=</span><span class="si">{</span><span class="n">response</span><span class="p">.</span><span class="nb">sum</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">response</span>                               <span class="c1"># 必须返回 response，否则客户端收不到结果
</span>

<span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">init</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="n">args</span><span class="p">)</span>                 <span class="c1"># 1. 初始化 rclpy（每个进程必须调用一次）
</span>    <span class="n">node</span> <span class="o">=</span> <span class="n">MinimalService</span><span class="p">()</span>               <span class="c1"># 2. 创建服务端节点（此时服务已注册）
</span>    <span class="n">rclpy</span><span class="p">.</span><span class="n">spin</span><span class="p">(</span><span class="n">node</span><span class="p">)</span>                      <span class="c1"># 3. 阻塞运行：一直监听并处理客户端请求
</span>    <span class="n">node</span><span class="p">.</span><span class="n">destroy_node</span><span class="p">()</span>                   <span class="c1"># 4. 清理：销毁节点（Ctrl+C 退出后执行）
</span>    <span class="n">rclpy</span><span class="p">.</span><span class="n">shutdown</span><span class="p">()</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="32-客户端-minimal_clientpy">3.2 客户端 <code class="language-plaintext highlighter-rouge">minimal_client.py</code></h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">sys</span>                      <span class="c1"># 读取命令行参数
</span><span class="kn">import</span> <span class="nn">rclpy</span>                    <span class="c1"># ROS 2 Python 客户端库
</span><span class="kn">from</span> <span class="nn">rclpy.node</span> <span class="kn">import</span> <span class="n">Node</span>     <span class="c1"># 节点基类
</span><span class="kn">from</span> <span class="nn">example_interfaces.srv</span> <span class="kn">import</span> <span class="n">AddTwoInts</span>  <span class="c1"># 服务接口类型（请求/响应的数据结构）
</span>

<span class="k">class</span> <span class="nc">MinimalClient</span><span class="p">(</span><span class="n">Node</span><span class="p">):</span>
    <span class="s">"""客户端节点：调用 add_two_ints 服务，计算 a + b"""</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="s">'minimal_client'</span><span class="p">)</span>           <span class="c1"># 节点名称（ros2 node list 可见）
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">cli</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">create_client</span><span class="p">(</span>               <span class="c1"># 创建客户端
</span>            <span class="n">AddTwoInts</span><span class="p">,</span>           <span class="c1"># 服务接口类型（与服务端保持一致）
</span>            <span class="s">'add_two_ints'</span><span class="p">)</span>       <span class="c1"># 服务名称（与服务端保持一致）
</span>        <span class="c1"># 等待服务端就绪：每秒检查一次，没等到就一直等（先启动客户端也不会报错）
</span>        <span class="k">while</span> <span class="ow">not</span> <span class="bp">self</span><span class="p">.</span><span class="n">cli</span><span class="p">.</span><span class="n">wait_for_service</span><span class="p">(</span><span class="n">timeout_sec</span><span class="o">=</span><span class="mf">1.0</span><span class="p">):</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">get_logger</span><span class="p">().</span><span class="n">info</span><span class="p">(</span><span class="s">'service not available, waiting again...'</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">call_service</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">):</span>
        <span class="c1"># 构造请求对象并给字段赋值（a、b 由调用方传入）
</span>        <span class="n">req</span> <span class="o">=</span> <span class="n">AddTwoInts</span><span class="p">.</span><span class="n">Request</span><span class="p">()</span>
        <span class="n">req</span><span class="p">.</span><span class="n">a</span> <span class="o">=</span> <span class="n">a</span>
        <span class="n">req</span><span class="p">.</span><span class="n">b</span> <span class="o">=</span> <span class="n">b</span>
        <span class="c1"># 异步发起请求：不阻塞主线程，返回一个 Future 对象，之后轮询它是否完成
</span>        <span class="n">future</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">cli</span><span class="p">.</span><span class="n">call_async</span><span class="p">(</span><span class="n">req</span><span class="p">)</span>
        <span class="c1"># 阻塞等待请求完成：等待期间仍会处理节点回调
</span>        <span class="n">rclpy</span><span class="p">.</span><span class="n">spin_until_future_complete</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">future</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">future</span><span class="p">.</span><span class="n">result</span><span class="p">()</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>   <span class="c1"># 成功拿到响应
</span>            <span class="bp">self</span><span class="p">.</span><span class="n">get_logger</span><span class="p">().</span><span class="n">info</span><span class="p">(</span>
                <span class="sa">f</span><span class="s">'Result: </span><span class="si">{</span><span class="n">req</span><span class="p">.</span><span class="n">a</span><span class="si">}</span><span class="s"> + </span><span class="si">{</span><span class="n">req</span><span class="p">.</span><span class="n">b</span><span class="si">}</span><span class="s"> = </span><span class="si">{</span><span class="n">future</span><span class="p">.</span><span class="n">result</span><span class="p">().</span><span class="nb">sum</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>
        <span class="k">else</span><span class="p">:</span>                              <span class="c1"># 调用失败（如服务端异常/超时）
</span>            <span class="bp">self</span><span class="p">.</span><span class="n">get_logger</span><span class="p">().</span><span class="n">error</span><span class="p">(</span><span class="s">'Service call failed'</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">init</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="n">args</span><span class="p">)</span>                 <span class="c1"># 1. 初始化 rclpy
</span>    <span class="c1"># 2. 解析命令行参数：a、b 取 sys.argv 前两个位置参数（缺省为 0）
</span>    <span class="n">a</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span> <span class="k">else</span> <span class="mi">0</span>
    <span class="n">b</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">2</span> <span class="k">else</span> <span class="mi">0</span>
    <span class="n">node</span> <span class="o">=</span> <span class="n">MinimalClient</span><span class="p">()</span>                <span class="c1"># 3. 创建客户端节点（内部已等待服务就绪）
</span>    <span class="n">node</span><span class="p">.</span><span class="n">call_service</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span>               <span class="c1"># 4. 发起调用并打印结果
</span>    <span class="n">node</span><span class="p">.</span><span class="n">destroy_node</span><span class="p">()</span>                   <span class="c1"># 5. 清理
</span>    <span class="n">rclpy</span><span class="p">.</span><span class="n">shutdown</span><span class="p">()</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="33-代码要点解读">3.3 代码要点解读</h3>

<table>
  <thead>
    <tr>
      <th>代码</th>
      <th>作用</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">rclpy.init()</code></td>
      <td>初始化 ROS 2 客户端库，<strong>每个进程必须调用一次</strong></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">create_service(Type, name, cb)</code></td>
      <td>创建服务端：接口类型 / 服务名 / 回调。回调返回 <code class="language-plaintext highlighter-rouge">response</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">create_client(Type, name)</code></td>
      <td>创建客户端：接口类型 / 服务名</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">wait_for_service(秒)</code></td>
      <td>阻塞等待服务端上线，返回 <code class="language-plaintext highlighter-rouge">bool</code>，用于客户端先于服务端启动的场景</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">AddTwoInts.Request()</code></td>
      <td>构造请求对象，为其字段（<code class="language-plaintext highlighter-rouge">a</code>、<code class="language-plaintext highlighter-rouge">b</code>）赋值（值来自命令行参数）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">call_async(req)</code></td>
      <td>异步发起请求，返回一个 <code class="language-plaintext highlighter-rouge">Future</code> 对象</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">spin_until_future_complete(node, future)</code></td>
      <td>在等待期间处理节点回调，直到请求完成；也可用 <code class="language-plaintext highlighter-rouge">future.result()</code> 拿结果</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>为什么回调要 <code class="language-plaintext highlighter-rouge">return response</code>？</strong> 服务端回调的签名固定为 <code class="language-plaintext highlighter-rouge">(request, response) -&gt; response</code>。你在回调里修改 <code class="language-plaintext highlighter-rouge">response</code> 的字段，最后<strong>必须把它返回</strong>，ROS 2 才会把结果送回客户端。</p>
</blockquote>

<blockquote>
  <p><strong><code class="language-plaintext highlighter-rouge">call_async</code> 是异步的</strong>：它不会阻塞主线程，而是返回 <code class="language-plaintext highlighter-rouge">Future</code>。用 <code class="language-plaintext highlighter-rouge">rclpy.spin_until_future_complete()</code> 或 <code class="language-plaintext highlighter-rouge">rclpy.spin_once()</code> + <code class="language-plaintext highlighter-rouge">future.done()</code> 来等待完成，这样节点在等待期间仍能处理其他回调。</p>
</blockquote>

<hr />

<h2 id="四完整过程从包到运行">四、完整过程（从包到运行）</h2>

<h3 id="第-1-步创建功能包">第 1 步：创建功能包</h3>

<p>在 <code class="language-plaintext highlighter-rouge">src/</code> 下用官方命令创建 Python 包（这里包名用 <code class="language-plaintext highlighter-rouge">py_service</code> 演示，也可换成你自己的名字）：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/ros_ws/src
ros2 pkg create py_service <span class="nt">--build-type</span> ament_python <span class="nt">--node-name</span> service
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--node-name service</code> 会自动生成 <code class="language-plaintext highlighter-rouge">py_service/service.py</code> 并在 <code class="language-plaintext highlighter-rouge">setup.py</code> 中注册入口。</p>

<h3 id="第-2-步放置代码文件">第 2 步：放置代码文件</h3>

<p>把上面两段代码分别保存为：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src/py_service/py_service/service.py     # 服务端
src/py_service/py_service/client.py      # 客户端
</code></pre></div></div>

<p>（也可以直接使用官方样例生成的 <code class="language-plaintext highlighter-rouge">service_member_function.py</code> / <code class="language-plaintext highlighter-rouge">client_member_function.py</code>，本文为你手写的是更精简的版本。）</p>

<h3 id="第-3-步配置-setuppy">第 3 步：配置 <code class="language-plaintext highlighter-rouge">setup.py</code></h3>

<p>编辑 <code class="language-plaintext highlighter-rouge">src/py_service/setup.py</code>，在 <code class="language-plaintext highlighter-rouge">console_scripts</code> 中注册两个可执行入口：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">entry_points</span><span class="o">=</span><span class="p">{</span>
    <span class="s">'console_scripts'</span><span class="p">:</span> <span class="p">[</span>
        <span class="s">'service = py_service.service:main'</span><span class="p">,</span>
        <span class="s">'client = py_service.client:main'</span><span class="p">,</span>
    <span class="p">],</span>
<span class="p">},</span>
</code></pre></div></div>

<p>格式为：<code class="language-plaintext highlighter-rouge">命令名 = 模块路径:函数名</code>。这样构建后就能用 <code class="language-plaintext highlighter-rouge">ros2 run py_service service</code> 直接启动。</p>

<h3 id="第-4-步确认-packagexml-依赖">第 4 步：确认 <code class="language-plaintext highlighter-rouge">package.xml</code> 依赖</h3>

<p>确保 <code class="language-plaintext highlighter-rouge">package.xml</code> 里声明了 <code class="language-plaintext highlighter-rouge">rclpy</code> 和 <code class="language-plaintext highlighter-rouge">example_interfaces</code>（<code class="language-plaintext highlighter-rouge">ros2 pkg create</code> 默认已带 <code class="language-plaintext highlighter-rouge">rclpy</code>，需手动补充 <code class="language-plaintext highlighter-rouge">example_interfaces</code>）：</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;exec_depend&gt;</span>rclpy<span class="nt">&lt;/exec_depend&gt;</span>
<span class="nt">&lt;exec_depend&gt;</span>example_interfaces<span class="nt">&lt;/exec_depend&gt;</span>
</code></pre></div></div>

<h3 id="第-5-步构建">第 5 步：构建</h3>

<p>回到工作区根目录，构建这个包（<strong>每次改代码后都要重新构建</strong>）：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/ros_ws
colcon build <span class="nt">--packages-select</span> py_service
<span class="nb">source install</span>/setup.bash
</code></pre></div></div>

<h3 id="第-6-步运行">第 6 步：运行</h3>

<p>开<strong>两个终端</strong>，<strong>先启动服务端，再启动客户端</strong>（客户端会等待服务端就绪，所以顺序颠倒也不会报错）：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 终端 1：服务端</span>
<span class="nb">source</span> /opt/ros/jazzy/setup.bash
<span class="nb">source</span> ~/ros_ws/install/setup.bash
ros2 run py_service service
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 终端 2：客户端（在命令末尾带上 a、b 两个整数参数，如 41 1）</span>
<span class="nb">source</span> /opt/ros/jazzy/setup.bash
<span class="nb">source</span> ~/ros_ws/install/setup.bash
ros2 run py_service client 41 1
</code></pre></div></div>

<h3 id="第-7-步验证结果">第 7 步：验证结果</h3>

<ul>
  <li><strong>客户端终端</strong>会打印：<code class="language-plaintext highlighter-rouge">Result: 41 + 1 = 42</code>，随后退出（<code class="language-plaintext highlighter-rouge">a</code>、<code class="language-plaintext highlighter-rouge">b</code> 取自命令行参数；不传参时默认为 <code class="language-plaintext highlighter-rouge">0</code>，会打印 <code class="language-plaintext highlighter-rouge">Result: 0 + 0 = 0</code>）。</li>
  <li><strong>服务端终端</strong>会打印：<code class="language-plaintext highlighter-rouge">Incoming request: a=41, b=1 -&gt; sum=42</code>。</li>
  <li>另开一个终端可以查看服务与节点信息：</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ros2 service list           <span class="c"># 查看所有服务，应包含 /add_two_ints</span>
ros2 service <span class="nb">type</span> /add_two_ints   <span class="c"># 查看服务类型：example_interfaces/srv/AddTwoInts</span>
ros2 node list              <span class="c"># 查看节点：/minimal_service /minimal_client</span>
</code></pre></div></div>

<ul>
  <li>也可以不写客户端，直接<strong>从命令行调用服务</strong>测试服务端：</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts <span class="s2">"{a: 5, b: 7}"</span>
</code></pre></div></div>

<p>预期输出示例：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 5, b: 7}"
requester: making request: example_interfaces.srv.AddTwoInts_Request(a=5, b=7)

response:
example_interfaces.srv.AddTwoInts_Response(sum=12)
</code></pre></div></div>

<hr />

<h2 id="五常见问题排查">五、常见问题排查</h2>

<table>
  <thead>
    <tr>
      <th>现象</th>
      <th>原因 / 解决</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ros2 run</code> 提示找不到包</td>
      <td>没 source 工作区：<code class="language-plaintext highlighter-rouge">source ~/ros_ws/install/setup.bash</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ros2 run</code> 提示找不到可执行文件</td>
      <td><code class="language-plaintext highlighter-rouge">setup.py</code> 的 <code class="language-plaintext highlighter-rouge">console_scripts</code> 没注册，或改后没重新 <code class="language-plaintext highlighter-rouge">colcon build</code></td>
    </tr>
    <tr>
      <td>客户端一直打印 “service not available”</td>
      <td>服务端没启动，或服务名不一致（两边必须都是 <code class="language-plaintext highlighter-rouge">add_two_ints</code>）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">future.result()</code> 为 <code class="language-plaintext highlighter-rouge">None</code></td>
      <td>服务端崩溃或调用超时，客户端会进入 <code class="language-plaintext highlighter-rouge">else</code> 分支打印 <code class="language-plaintext highlighter-rouge">Service call failed</code></td>
    </tr>
    <tr>
      <td>服务端回调没返回 <code class="language-plaintext highlighter-rouge">response</code></td>
      <td>回调必须 <code class="language-plaintext highlighter-rouge">return response</code>，否则客户端收不到结果、会一直卡住</td>
    </tr>
    <tr>
      <td>改了代码但行为没变</td>
      <td>忘记重新构建：<code class="language-plaintext highlighter-rouge">colcon build --packages-select py_service</code></td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="六小结">六、小结</h2>

<ul>
  <li><strong>服务端 / 客户端</strong>是 ROS 2 中”一问一答”的<strong>同步</strong>通信模式，通过<strong>服务名 + 服务接口（srv）</strong>关联，适合一次性调用拿结果的场景。</li>
  <li>用 Python 只需掌握 <code class="language-plaintext highlighter-rouge">create_service</code> / <code class="language-plaintext highlighter-rouge">create_client</code>、<code class="language-plaintext highlighter-rouge">wait_for_service</code>、<code class="language-plaintext highlighter-rouge">call_async</code> + <code class="language-plaintext highlighter-rouge">spin_until_future_complete</code> 几个关键点。</li>
  <li>完整流程：<strong>建包 → 写代码 → 注册入口 → 补充依赖 → 构建 → 运行</strong>，每一步缺一不可。</li>
  <li>本文使用现成的 <code class="language-plaintext highlighter-rouge">AddTwoInts</code> 接口。当你要传递<strong>自己的数据结构</strong>时，需要自定义 <code class="language-plaintext highlighter-rouge">.srv</code> 文件（格式为 <code class="language-plaintext highlighter-rouge">请求字段</code> + <code class="language-plaintext highlighter-rouge">---</code> + <code class="language-plaintext highlighter-rouge">响应字段</code>）并在 <code class="language-plaintext highlighter-rouge">package.xml</code> 中添加 <code class="language-plaintext highlighter-rouge">rosidl_default_generators</code> 等依赖，这部分可以留待进阶文章展开。</li>
</ul>

<p>掌握了 Service，你就掌握了 ROS 2 里”同步调用”的通信方式；它与前面学的 Pub/Sub（异步）、以及后续要学的 Action（长耗时任务）一起，构成了 ROS 2 节点通信的三大核心模式。</p>]]></content><author><name>老张同志</name></author><category term="ros2" /><category term="tutorial" /><summary type="html"><![CDATA[从零掌握 ROS 2 服务/客户端（Service）通信模式：理解服务端与客户端的概念与特点，用 Python 手写最小可运行的服务端（minimal_service）与客户端（minimal_client），覆盖建包、注册入口、构建、运行与命令行调用验证，附常见问题排查表。]]></summary></entry><entry><title type="html">ROS 2 功能包：C++ 包与 Python 包的文件结构与内容介绍</title><link href="https://1q08.github.io/ros_ws/ros2/tutorial/2026/08/21/ros2-package-structure.html" rel="alternate" type="text/html" title="ROS 2 功能包：C++ 包与 Python 包的文件结构与内容介绍" /><published>2026-08-21T03:40:00+00:00</published><updated>2026-08-21T03:40:00+00:00</updated><id>https://1q08.github.io/ros_ws/ros2/tutorial/2026/08/21/ros2-package-structure</id><content type="html" xml:base="https://1q08.github.io/ros_ws/ros2/tutorial/2026/08/21/ros2-package-structure.html"><![CDATA[<h1 id="ros-2-功能包c-包与-python-包的文件结构与内容介绍">ROS 2 功能包：C++ 包与 Python 包的文件结构与内容介绍</h1>

<p>本文分别介绍 ROS 2 中 <strong>C++ 功能包</strong>（<code class="language-plaintext highlighter-rouge">ament_cmake</code>）和 <strong>Python 功能包</strong>（<code class="language-plaintext highlighter-rouge">ament_python</code>）的目录结构、每个文件的用途，以及两者的区别。</p>

<blockquote>
  <p>环境：ROS 2 Jazzy / Ubuntu，工作区：<code class="language-plaintext highlighter-rouge">~/ros_ws</code>（<code class="language-plaintext highlighter-rouge">src/</code> 下存放所有功能包源码）。</p>
</blockquote>

<hr />

<h2 id="〇总览两类包的最简结构对比">〇、总览：两类包的最简结构对比</h2>

<table>
  <thead>
    <tr>
      <th>文件/目录</th>
      <th>C++ 包（<code class="language-plaintext highlighter-rouge">ament_cmake</code>）</th>
      <th>Python 包（<code class="language-plaintext highlighter-rouge">ament_python</code>）</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>包描述文件</td>
      <td><code class="language-plaintext highlighter-rouge">package.xml</code></td>
      <td><code class="language-plaintext highlighter-rouge">package.xml</code></td>
    </tr>
    <tr>
      <td>构建配置</td>
      <td><code class="language-plaintext highlighter-rouge">CMakeLists.txt</code></td>
      <td><code class="language-plaintext highlighter-rouge">setup.py</code> + <code class="language-plaintext highlighter-rouge">setup.cfg</code></td>
    </tr>
    <tr>
      <td>源码目录</td>
      <td><code class="language-plaintext highlighter-rouge">src/</code> + <code class="language-plaintext highlighter-rouge">include/</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;包名&gt;/</code>（同名包目录）</td>
    </tr>
    <tr>
      <td>头文件（.hpp）</td>
      <td><code class="language-plaintext highlighter-rouge">include/&lt;包名&gt;/</code></td>
      <td>无（Python 无需头文件）</td>
    </tr>
    <tr>
      <td>节点可执行文件入口</td>
      <td><code class="language-plaintext highlighter-rouge">add_executable()</code></td>
      <td><code class="language-plaintext highlighter-rouge">setup.py</code> 的 <code class="language-plaintext highlighter-rouge">console_scripts</code></td>
    </tr>
    <tr>
      <td>launch 文件</td>
      <td><code class="language-plaintext highlighter-rouge">launch/</code></td>
      <td><code class="language-plaintext highlighter-rouge">launch/</code></td>
    </tr>
    <tr>
      <td>参数/配置文件</td>
      <td><code class="language-plaintext highlighter-rouge">config/</code></td>
      <td><code class="language-plaintext highlighter-rouge">config/</code>（或 <code class="language-plaintext highlighter-rouge">launch/</code>）</td>
    </tr>
    <tr>
      <td>测试</td>
      <td><code class="language-plaintext highlighter-rouge">test/</code></td>
      <td><code class="language-plaintext highlighter-rouge">test/</code></td>
    </tr>
    <tr>
      <td>构建类型标识</td>
      <td><code class="language-plaintext highlighter-rouge">package.xml</code> 里 <code class="language-plaintext highlighter-rouge">&lt;build_type&gt;ament_cmake&lt;/build_type&gt;</code></td>
      <td><code class="language-plaintext highlighter-rouge">package.xml</code> 里 <code class="language-plaintext highlighter-rouge">&lt;build_type&gt;ament_python&lt;/build_type&gt;</code></td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>一句话总结</strong>：C++ 包用 <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> 编译成可执行文件；Python 包用 <code class="language-plaintext highlighter-rouge">setup.py</code> 把脚本注册为可执行入口点。两者都靠 <code class="language-plaintext highlighter-rouge">package.xml</code> 描述包信息和依赖。</p>
</blockquote>

<hr />

<h2 id="一python-功能包ament_python">一、Python 功能包（<code class="language-plaintext highlighter-rouge">ament_python</code>）</h2>

<h3 id="11-完整目录结构">1.1 完整目录结构</h3>

<p>以一个最小功能包 <code class="language-plaintext highlighter-rouge">my_package</code> 为例：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>my_package/                      # 功能包根目录（与包名一致）
├── package.xml                  # 包描述文件：包名、版本、依赖、构建类型
├── setup.py                     # 打包配置：安装规则 + 可执行入口点
├── setup.cfg                    # 打包辅助配置（声明构建类型、脚本安装位置）
├── resource/
│   └── my_package               # 资源索引文件（内容为包名，供 ament 索引）
├── my_package/                  # 同名源码包目录（Python 源码都放这里）
│   ├── __init__.py              # 空文件，标记这是一个 Python 包
│   └── my_node.py               # 节点源码（入口点 main()）
├── launch/
│   └── mylaunch.launch.py       # launch 文件（可选）
└── test/                        # 测试目录（可选）
    ├── test_copyright.py
    ├── test_flake8.py
    └── test_pep257.py
</code></pre></div></div>

<h3 id="12-各文件作用">1.2 各文件作用</h3>

<table>
  <thead>
    <tr>
      <th>文件</th>
      <th>作用</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">package.xml</code></td>
      <td>声明包名、版本、依赖、<strong>构建类型 <code class="language-plaintext highlighter-rouge">ament_python</code></strong>。<code class="language-plaintext highlighter-rouge">ros2 pkg</code> 和 colcon 都靠它识别包。</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">setup.py</code></td>
      <td><strong>最关键</strong>。声明 <code class="language-plaintext highlighter-rouge">data_files</code> 安装规则（把 package.xml、launch 文件装到 <code class="language-plaintext highlighter-rouge">share/</code> 下）和 <code class="language-plaintext highlighter-rouge">console_scripts</code>（把 <code class="language-plaintext highlighter-rouge">.py</code> 注册成可执行命令）。</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">setup.cfg</code></td>
      <td>告知 setuptools 这是 <code class="language-plaintext highlighter-rouge">ament_python</code> 类型、脚本装到 <code class="language-plaintext highlighter-rouge">lib/&lt;包名&gt;</code> 下。</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">resource/&lt;包名&gt;</code></td>
      <td>一个内容为包名的空文件，供 ament 资源索引定位包。</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;包名&gt;/</code> 目录</td>
      <td>Python 源码所在目录，里面的 <code class="language-plaintext highlighter-rouge">.py</code> 就是各节点程序。</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">__init__.py</code></td>
      <td>空文件即可，标记该目录是 Python 包。</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">launch/</code></td>
      <td>存放 <code class="language-plaintext highlighter-rouge">.launch.py</code>（需在 <code class="language-plaintext highlighter-rouge">setup.py</code> 里声明安装规则才能被 <code class="language-plaintext highlighter-rouge">ros2 launch</code> 找到）。</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">test/</code></td>
      <td>单元测试（copyright/flake8/pep257 等，由模板自动生成）。</td>
    </tr>
  </tbody>
</table>

<h3 id="13-setuppy-详解">1.3 <code class="language-plaintext highlighter-rouge">setup.py</code> 详解</h3>

<p>以 <code class="language-plaintext highlighter-rouge">my_package/setup.py</code> 为例（该文件含详细中文注释）：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># ============================================================
# 文件作用：setup.py 是 ROS 2 Python 功能包（ament_python）的
# 构建配置文件，用于向 setuptools/colcon 声明包的元信息、依赖、
# 数据文件以及可执行入口（console_scripts）。
#   - colcon build 时据此生成可安装的 Python 包
#   - console_scripts 决定 ros2 run &lt;包名&gt; &lt;节点名&gt; 能运行哪些节点
#   - 与 package.xml 一起构成 Python 功能包的必要文件
# ============================================================
</span><span class="kn">from</span> <span class="nn">setuptools</span> <span class="kn">import</span> <span class="n">find_packages</span><span class="p">,</span> <span class="n">setup</span>
<span class="kn">import</span> <span class="nn">os</span>  <span class="c1"># 拼接文件路径
</span><span class="kn">from</span> <span class="nn">glob</span> <span class="kn">import</span> <span class="n">glob</span>  <span class="c1"># 用通配符匹配 launch 文件
</span>
<span class="n">package_name</span> <span class="o">=</span> <span class="s">'my_package'</span>  <span class="c1"># 包名：须与 src 目录名、package.xml 中的 &lt;name&gt; 一致
</span>
<span class="n">setup</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="n">package_name</span><span class="p">,</span>                         <span class="c1"># 包的安装名称（对应包名）
</span>    <span class="n">version</span><span class="o">=</span><span class="s">'1.1.0'</span><span class="p">,</span>                           <span class="c1"># 版本号：遵循语义化版本（major.minor.patch）
</span>    <span class="n">packages</span><span class="o">=</span><span class="n">find_packages</span><span class="p">(</span><span class="n">exclude</span><span class="o">=</span><span class="p">[</span><span class="s">'test'</span><span class="p">]),</span>  <span class="c1"># 自动发现要打包的 Python 子包，排除 test 目录
</span>    <span class="n">data_files</span><span class="o">=</span><span class="p">[</span>
        <span class="c1"># 安装 ament 资源索引文件，用于 ros2 pkg 识别该包
</span>        <span class="p">(</span><span class="s">'share/ament_index/resource_index/packages'</span><span class="p">,</span>
            <span class="p">[</span><span class="s">'resource/'</span> <span class="o">+</span> <span class="n">package_name</span><span class="p">]),</span>
        <span class="c1"># 安装 package.xml 到共享目录，供构建系统读取包元信息
</span>        <span class="p">(</span><span class="s">'share/'</span> <span class="o">+</span> <span class="n">package_name</span><span class="p">,</span> <span class="p">[</span><span class="s">'package.xml'</span><span class="p">]),</span>
        <span class="c1"># 安装 launch 目录下的所有 launch 文件到 share/&lt;包名&gt;/launch/
</span>        <span class="c1"># 使 ros2 launch &lt;包名&gt; &lt;launch文件名&gt; 能通过包名找到 launch 文件
</span>        <span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="s">'share'</span><span class="p">,</span> <span class="n">package_name</span><span class="p">,</span> <span class="s">'launch'</span><span class="p">),</span> <span class="n">glob</span><span class="p">(</span><span class="s">'launch/*.launch.py'</span><span class="p">)),</span>
    <span class="p">],</span>
    <span class="n">install_requires</span><span class="o">=</span><span class="p">[</span><span class="s">'setuptools'</span><span class="p">],</span>         <span class="c1"># 运行时依赖的 Python 包
</span>    <span class="n">zip_safe</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>                           <span class="c1"># 允许以 zip 形式打包（纯 Python 包可设为 True）
</span>    <span class="n">maintainer</span><span class="o">=</span><span class="s">'nvidia'</span><span class="p">,</span>                     <span class="c1"># 维护者姓名
</span>    <span class="n">maintainer_email</span><span class="o">=</span><span class="s">'example@163.com'</span><span class="p">,</span>      <span class="c1"># 维护者邮箱
</span>    <span class="n">description</span><span class="o">=</span><span class="s">'最小的 ROS 2 Python 功能包'</span><span class="p">,</span>  <span class="c1"># 包的功能用途描述
</span>    <span class="n">license</span><span class="o">=</span><span class="s">'Apache License 2.0'</span><span class="p">,</span>            <span class="c1"># 开源协议声明
</span>    <span class="n">extras_require</span><span class="o">=</span><span class="p">{</span>                         <span class="c1"># 额外依赖：仅安装 test 扩展时才装 pytest
</span>        <span class="s">'test'</span><span class="p">:</span> <span class="p">[</span>
            <span class="s">'pytest'</span><span class="p">,</span>
        <span class="p">],</span>
    <span class="p">},</span>
    <span class="n">entry_points</span><span class="o">=</span><span class="p">{</span>
        <span class="s">'console_scripts'</span><span class="p">:</span> <span class="p">[</span>
            <span class="c1"># '可执行命令名' = '包名.脚本文件名:主函数名'
</span>            <span class="s">'my_node = my_package.my_node:main'</span>
        <span class="p">],</span>
    <span class="p">},</span>
<span class="p">)</span>
</code></pre></div></div>

<blockquote>
  <p><strong>要点</strong>：</p>
  <ul>
    <li><code class="language-plaintext highlighter-rouge">console_scripts</code> 决定你能用 <code class="language-plaintext highlighter-rouge">ros2 run my_package my_node</code> 运行什么。<code class="language-plaintext highlighter-rouge">= 左边</code> 是命令名，<code class="language-plaintext highlighter-rouge">右边</code> 是 <code class="language-plaintext highlighter-rouge">包名.文件名:函数</code>。</li>
    <li>不写 <code class="language-plaintext highlighter-rouge">data_files</code> 里 launch 的安装规则，<code class="language-plaintext highlighter-rouge">ros2 launch</code> 会报”文件找不到”。</li>
  </ul>
</blockquote>

<h3 id="14-packagexml-详解">1.4 <code class="language-plaintext highlighter-rouge">package.xml</code> 详解</h3>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!--
  文件作用：package.xml 是 ROS 2 功能包（Package）的清单（Manifest）文件，
  用于声明包的基本信息（名称、版本、维护者、许可证）以及构建/运行/测试时
  的依赖关系，是 ament_python 构建系统识别和构建包的必需文件。

  - colcon 根据本文件判断包名、构建类型与依赖
  - ros2 pkg xml &lt;包名&gt; 可直接输出本文件内容
--&gt;</span>
<span class="cp">&lt;?xml version="1.0"?&gt;</span>
<span class="nt">&lt;package</span> <span class="na">format=</span><span class="s">"3"</span><span class="nt">&gt;</span>
  <span class="c">&lt;!-- 包名：必须与 src 下的目录名一致，且全小写、下划线分隔 --&gt;</span>
  <span class="nt">&lt;name&gt;</span>my_package<span class="nt">&lt;/name&gt;</span>
  <span class="c">&lt;!-- 版本号：遵循语义化版本（major.minor.patch）规范 --&gt;</span>
  <span class="nt">&lt;version&gt;</span>1.1.0<span class="nt">&lt;/version&gt;</span>
  <span class="c">&lt;!-- 包的简要描述，用于说明该包的功能用途 --&gt;</span>
  <span class="nt">&lt;description&gt;</span>最小的 ROS 2 Python 功能包<span class="nt">&lt;/description&gt;</span>
  <span class="c">&lt;!-- 维护者：负责维护该包的人，email 属性必填 --&gt;</span>
  <span class="nt">&lt;maintainer</span> <span class="na">email=</span><span class="s">"example@163.com"</span><span class="nt">&gt;</span>nvidia<span class="nt">&lt;/maintainer&gt;</span>
  <span class="c">&lt;!-- 许可证：声明包的开源协议，如 Apache-2.0 / MIT / BSD-3-Clause --&gt;</span>
  <span class="nt">&lt;license&gt;</span>Apache License 2.0<span class="nt">&lt;/license&gt;</span>

  <span class="c">&lt;!-- 以下为测试阶段依赖（构建/运行不需要，仅跑测试时才用） --&gt;</span>
  <span class="nt">&lt;test_depend&gt;</span>ament_copyright<span class="nt">&lt;/test_depend&gt;</span>
  <span class="nt">&lt;test_depend&gt;</span>ament_flake8<span class="nt">&lt;/test_depend&gt;</span>
  <span class="nt">&lt;test_depend&gt;</span>ament_pep257<span class="nt">&lt;/test_depend&gt;</span>
  <span class="nt">&lt;test_depend&gt;</span>python3-pytest<span class="nt">&lt;/test_depend&gt;</span>

  <span class="nt">&lt;export&gt;</span>
    <span class="c">&lt;!-- 构建类型：ament_python 表示这是一个 Python 功能包 --&gt;</span>
    <span class="nt">&lt;build_type&gt;</span>ament_python<span class="nt">&lt;/build_type&gt;</span>
  <span class="nt">&lt;/export&gt;</span>
<span class="nt">&lt;/package&gt;</span>
</code></pre></div></div>

<h3 id="15-创建构建与运行">1.5 创建、构建与运行</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. 创建 Python 功能包（模板自动生成上述所有文件）</span>
<span class="nb">cd</span> ~/ros_ws/src
ros2 pkg create my_package <span class="nt">--build-type</span> ament_python <span class="nt">--node-name</span> my_node

<span class="c"># 2. 构建（必须回工作区根目录）</span>
<span class="nb">cd</span> ~/ros_ws
colcon build <span class="nt">--packages-select</span> my_package

<span class="c"># 3. source 环境</span>
<span class="nb">source install</span>/setup.bash

<span class="c"># 4. 运行节点</span>
ros2 run my_package my_node

<span class="c"># 5. 运行 launch 文件（需要 setup.py 里有 launch 安装规则）</span>
ros2 launch my_package mylaunch.launch.py
</code></pre></div></div>

<hr />

<h2 id="二c-功能包ament_cmake">二、C++ 功能包（<code class="language-plaintext highlighter-rouge">ament_cmake</code>）</h2>

<h3 id="21-完整目录结构">2.1 完整目录结构</h3>

<p>以一个发布/订阅示例包 <code class="language-plaintext highlighter-rouge">cpp_pubsub</code> 为例：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cpp_pubsub/                         # 功能包根目录（与包名一致）
├── package.xml                     # 包描述文件：包名、版本、依赖、构建类型
├── CMakeLists.txt                  # 构建配置：编译、链接、安装（C++ 包的核心）
├── resource/
│   └── cpp_pubsub                  # 资源索引文件（内容为包名）
├── include/
│   └── cpp_pubsub/                 # 头文件目录（命名空间 = 包名）
│       └── publisher.hpp           # 发布者类声明（头文件）
├── src/
│   ├── publisher.cpp               # 发布者实现
│   ├── subscriber.cpp              # 订阅者实现
│   └── main.cpp                    # 主程序入口（调用类）
├── launch/
│   └── pubsub.launch.py            # launch 文件（可选，但很常用）
├── config/
│   └── params.yaml                 # 参数配置文件（可选）
└── test/                           # 测试目录（可选）
    ├── test_copyright.cmake
    └── test_cpplint.py
</code></pre></div></div>

<h3 id="22-各文件作用">2.2 各文件作用</h3>

<table>
  <thead>
    <tr>
      <th>文件</th>
      <th>作用</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">package.xml</code></td>
      <td>声明包名、版本、依赖、<strong>构建类型 <code class="language-plaintext highlighter-rouge">ament_cmake</code></strong>。</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CMakeLists.txt</code></td>
      <td><strong>最关键</strong>。<code class="language-plaintext highlighter-rouge">find_package()</code> 找依赖、<code class="language-plaintext highlighter-rouge">add_executable()</code> 编译源文件、<code class="language-plaintext highlighter-rouge">ament_target_dependencies()</code> 链接 ROS 依赖、<code class="language-plaintext highlighter-rouge">install()</code> 安装可执行文件和 launch 文件。</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">include/&lt;包名&gt;/</code></td>
      <td>头文件（<code class="language-plaintext highlighter-rouge">.hpp</code>）目录，通常与包名同名、并用命名空间包裹。</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">src/</code></td>
      <td>源文件（<code class="language-plaintext highlighter-rouge">.cpp</code>）目录，包含每个节点的实现和 <code class="language-plaintext highlighter-rouge">main()</code>。</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">resource/&lt;包名&gt;</code></td>
      <td>内容为包名的空文件，供 ament 索引。</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">launch/</code></td>
      <td>launch 文件（需在 <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> 里 <code class="language-plaintext highlighter-rouge">install(DIRECTORY launch ...)</code>）。</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">config/</code></td>
      <td>配置文件（如参数 YAML），需在 <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> 里 <code class="language-plaintext highlighter-rouge">install(DIRECTORY config ...)</code>。</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">test/</code></td>
      <td>测试（copyright/cpplint 等模板文件）。</td>
    </tr>
  </tbody>
</table>

<h3 id="23-cmakeliststxt-详解">2.3 <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> 详解</h3>

<div class="language-cmake highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cmake_minimum_required</span><span class="p">(</span>VERSION 3.8<span class="p">)</span>
<span class="nb">project</span><span class="p">(</span>cpp_pubsub<span class="p">)</span>                    <span class="c1"># 项目名 = 包名</span>

<span class="c1"># 使用 C++17</span>
<span class="nb">if</span><span class="p">(</span>CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES <span class="s2">"Clang"</span><span class="p">)</span>
  <span class="nb">add_compile_options</span><span class="p">(</span>-Wall -Wextra -Wpedantic<span class="p">)</span>
<span class="nb">endif</span><span class="p">()</span>
<span class="nb">set</span><span class="p">(</span>CMAKE_CXX_STANDARD 17<span class="p">)</span>

<span class="c1"># 找依赖（rclcpp：C++ 的 ROS 2 客户端库；std_msgs：标准消息）</span>
<span class="nb">find_package</span><span class="p">(</span>ament_cmake REQUIRED<span class="p">)</span>
<span class="nb">find_package</span><span class="p">(</span>rclcpp REQUIRED<span class="p">)</span>
<span class="nb">find_package</span><span class="p">(</span>std_msgs REQUIRED<span class="p">)</span>

<span class="c1"># 编译发布者可执行文件：publisher 命令 &lt;- src/publisher.cpp + main.cpp</span>
<span class="nb">add_executable</span><span class="p">(</span>publisher src/publisher.cpp src/main.cpp<span class="p">)</span>
<span class="c1"># 链接 ROS 依赖（自动带上 include 路径）</span>
<span class="nf">ament_target_dependencies</span><span class="p">(</span>publisher rclcpp std_msgs<span class="p">)</span>

<span class="c1"># 编译订阅者</span>
<span class="nb">add_executable</span><span class="p">(</span>subscriber src/subscriber.cpp<span class="p">)</span>
<span class="nf">ament_target_dependencies</span><span class="p">(</span>subscriber rclcpp std_msgs<span class="p">)</span>

<span class="c1"># 安装可执行文件（不装的话 ros2 run 找不到）</span>
<span class="nb">install</span><span class="p">(</span>TARGETS
  publisher
  subscriber
  DESTINATION lib/<span class="si">${</span><span class="nv">PROJECT_NAME</span><span class="si">}</span>
<span class="p">)</span>

<span class="c1"># 安装 launch 目录（ros2 launch 才能找到）</span>
<span class="nb">install</span><span class="p">(</span>DIRECTORY launch
  DESTINATION share/<span class="si">${</span><span class="nv">PROJECT_NAME</span><span class="si">}</span>
<span class="p">)</span>

<span class="c1"># 安装 config 目录</span>
<span class="nb">install</span><span class="p">(</span>DIRECTORY config
  DESTINATION share/<span class="si">${</span><span class="nv">PROJECT_NAME</span><span class="si">}</span>
<span class="p">)</span>

<span class="nb">if</span><span class="p">(</span>BUILD_TESTING<span class="p">)</span>
  <span class="nb">find_package</span><span class="p">(</span>ament_lint_auto REQUIRED<span class="p">)</span>
  <span class="nf">ament_lint_auto_find_test_dependencies</span><span class="p">()</span>
<span class="nb">endif</span><span class="p">()</span>

<span class="nf">ament_package</span><span class="p">()</span>
</code></pre></div></div>

<blockquote>
  <p><strong>要点</strong>：</p>
  <ul>
    <li><strong>一个 <code class="language-plaintext highlighter-rouge">add_executable()</code> 对应一个可执行文件</strong>，也就是 <code class="language-plaintext highlighter-rouge">ros2 run cpp_pubsub publisher</code> 里的 <code class="language-plaintext highlighter-rouge">publisher</code>。</li>
    <li><code class="language-plaintext highlighter-rouge">ament_target_dependencies()</code> 负责把 ROS 库的 include 路径、链接库都配好，必须为每个可执行文件调用。</li>
    <li><code class="language-plaintext highlighter-rouge">install()</code> 三件套：可执行文件 → <code class="language-plaintext highlighter-rouge">lib/</code>，launch/config → <code class="language-plaintext highlighter-rouge">share/</code>。漏掉任何一个，<code class="language-plaintext highlighter-rouge">ros2 run</code>/<code class="language-plaintext highlighter-rouge">ros2 launch</code> 都会报”找不到”。</li>
  </ul>
</blockquote>

<h3 id="24-packagexml-详解">2.4 <code class="language-plaintext highlighter-rouge">package.xml</code> 详解</h3>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0"?&gt;</span>
<span class="nt">&lt;package</span> <span class="na">format=</span><span class="s">"3"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;name&gt;</span>cpp_pubsub<span class="nt">&lt;/name&gt;</span>
  <span class="nt">&lt;version&gt;</span>1.1.0<span class="nt">&lt;/version&gt;</span>
  <span class="nt">&lt;description&gt;</span>C++ 发布/订阅示例<span class="nt">&lt;/description&gt;</span>
  <span class="nt">&lt;maintainer</span> <span class="na">email=</span><span class="s">"example@163.com"</span><span class="nt">&gt;</span>nvidia<span class="nt">&lt;/maintainer&gt;</span>
  <span class="nt">&lt;license&gt;</span>Apache License 2.0<span class="nt">&lt;/license&gt;</span>

  <span class="c">&lt;!-- 编译时依赖 --&gt;</span>
  <span class="nt">&lt;buildtool_depend&gt;</span>ament_cmake<span class="nt">&lt;/buildtool_depend&gt;</span>

  <span class="c">&lt;!-- 运行时依赖 --&gt;</span>
  <span class="nt">&lt;exec_depend&gt;</span>rclcpp<span class="nt">&lt;/exec_depend&gt;</span>
  <span class="nt">&lt;exec_depend&gt;</span>std_msgs<span class="nt">&lt;/exec_depend&gt;</span>

  <span class="c">&lt;!-- 测试依赖 --&gt;</span>
  <span class="nt">&lt;test_depend&gt;</span>ament_lint_auto<span class="nt">&lt;/test_depend&gt;</span>
  <span class="nt">&lt;test_depend&gt;</span>ament_lint_common<span class="nt">&lt;/test_depend&gt;</span>

  <span class="nt">&lt;export&gt;</span>
    <span class="c">&lt;!-- 构建类型：C++ 包必须写 ament_cmake --&gt;</span>
    <span class="nt">&lt;build_type&gt;</span>ament_cmake<span class="nt">&lt;/build_type&gt;</span>
  <span class="nt">&lt;/export&gt;</span>
<span class="nt">&lt;/package&gt;</span>
</code></pre></div></div>

<blockquote>
  <p><strong>与 Python 包的差别</strong>：C++ 包多了 <code class="language-plaintext highlighter-rouge">&lt;buildtool_depend&gt;ament_cmake&lt;/buildtool_depend&gt;</code>（构建工具依赖），运行时依赖是 <code class="language-plaintext highlighter-rouge">rclcpp</code> 而不是 <code class="language-plaintext highlighter-rouge">rclpy</code>。</p>
</blockquote>

<h3 id="25-源码结构示例">2.5 源码结构示例</h3>

<p>头文件 <code class="language-plaintext highlighter-rouge">include/cpp_pubsub/publisher.hpp</code>：</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#ifndef CPP_PUBSUB__PUBLISHER_HPP_      // 防止重复包含
#define CPP_PUBSUB__PUBLISHER_HPP_
</span>
<span class="cp">#include</span> <span class="cpf">&lt;rclcpp/rclcpp.hpp&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;std_msgs/msg/string.hpp&gt;</span><span class="cp">
</span>
<span class="k">namespace</span> <span class="n">cpp_pubsub</span> <span class="p">{</span>                 <span class="c1">// 命名空间与包名一致</span>

<span class="k">class</span> <span class="nc">Publisher</span> <span class="o">:</span> <span class="k">public</span> <span class="n">rclcpp</span><span class="o">::</span><span class="n">Node</span> <span class="p">{</span>
<span class="nl">public:</span>
  <span class="n">Publisher</span><span class="p">();</span>                          <span class="c1">// 构造函数：创建节点、话题、定时器</span>

<span class="nl">private:</span>
  <span class="n">rclcpp</span><span class="o">::</span><span class="n">Publisher</span><span class="o">&lt;</span><span class="n">std_msgs</span><span class="o">::</span><span class="n">msg</span><span class="o">::</span><span class="n">String</span><span class="o">&gt;::</span><span class="n">SharedPtr</span> <span class="n">publisher_</span><span class="p">;</span>
  <span class="n">rclcpp</span><span class="o">::</span><span class="n">TimerBase</span><span class="o">::</span><span class="n">SharedPtr</span> <span class="n">timer_</span><span class="p">;</span>
  <span class="kt">size_t</span> <span class="n">count_</span><span class="p">;</span>
<span class="p">};</span>

<span class="p">}</span>  <span class="c1">// namespace cpp_pubsub</span>

<span class="cp">#endif  // CPP_PUBSUB__PUBLISHER_HPP_
</span></code></pre></div></div>

<p>源文件 <code class="language-plaintext highlighter-rouge">src/publisher.cpp</code>：</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"cpp_pubsub/publisher.hpp"</span><span class="cp">
</span>
<span class="k">namespace</span> <span class="n">cpp_pubsub</span> <span class="p">{</span>

<span class="n">Publisher</span><span class="o">::</span><span class="n">Publisher</span><span class="p">()</span> <span class="o">:</span> <span class="n">Node</span><span class="p">(</span><span class="s">"publisher"</span><span class="p">)</span> <span class="p">{</span>          <span class="c1">// 节点名 publisher</span>
  <span class="n">publisher_</span> <span class="o">=</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">create_publisher</span><span class="o">&lt;</span><span class="n">std_msgs</span><span class="o">::</span><span class="n">msg</span><span class="o">::</span><span class="n">String</span><span class="o">&gt;</span><span class="p">(</span><span class="s">"topic"</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
  <span class="n">timer_</span> <span class="o">=</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">create_wall_timer</span><span class="p">(</span>
      <span class="n">std</span><span class="o">::</span><span class="n">chrono</span><span class="o">::</span><span class="n">seconds</span><span class="p">(</span><span class="mi">1</span><span class="p">),</span> <span class="p">[</span><span class="k">this</span><span class="p">]()</span> <span class="p">{</span>             <span class="c1">// 每秒发布一次</span>
        <span class="k">auto</span> <span class="n">msg</span> <span class="o">=</span> <span class="n">std_msgs</span><span class="o">::</span><span class="n">msg</span><span class="o">::</span><span class="n">String</span><span class="p">();</span>
        <span class="n">msg</span><span class="p">.</span><span class="n">data</span> <span class="o">=</span> <span class="s">"Hello, world! "</span> <span class="o">+</span> <span class="n">std</span><span class="o">::</span><span class="n">to_string</span><span class="p">(</span><span class="n">count_</span><span class="o">++</span><span class="p">);</span>
        <span class="n">publisher_</span><span class="o">-&gt;</span><span class="n">publish</span><span class="p">(</span><span class="n">msg</span><span class="p">);</span>
      <span class="p">});</span>
<span class="p">}</span>

<span class="p">}</span>  <span class="c1">// namespace cpp_pubsub</span>
</code></pre></div></div>

<blockquote>
  <p><strong>C++ 与 Python 的对应关系</strong>：</p>
  <ul>
    <li>类的构造函数 <code class="language-plaintext highlighter-rouge">Node("publisher")</code> ≈ Python 的 <code class="language-plaintext highlighter-rouge">Node('publisher')</code></li>
    <li><code class="language-plaintext highlighter-rouge">create_publisher</code> / <code class="language-plaintext highlighter-rouge">create_subscription</code> / <code class="language-plaintext highlighter-rouge">create_wall_timer</code> 的 API 与 Python 一一对应，只是写法从 Python 脚本变成了编译型 C++。</li>
  </ul>
</blockquote>

<h3 id="26-创建构建与运行">2.6 创建、构建与运行</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. 创建 C++ 功能包</span>
<span class="nb">cd</span> ~/ros_ws/src
ros2 pkg create cpp_pubsub <span class="nt">--build-type</span> ament_cmake <span class="nt">--node-name</span> publisher

<span class="c"># 2. 构建（先装依赖的 ROS 库）</span>
<span class="nb">cd</span> ~/ros_ws
colcon build <span class="nt">--packages-select</span> cpp_pubsub

<span class="c"># 3. source 环境</span>
<span class="nb">source install</span>/setup.bash

<span class="c"># 4. 运行节点</span>
ros2 run cpp_pubsub publisher

<span class="c"># 5. 运行 launch 文件（需要 CMakeLists.txt 里有 install 规则）</span>
ros2 launch cpp_pubsub pubsub.launch.py
</code></pre></div></div>

<hr />

<h2 id="三两类包的对比总结">三、两类包的对比总结</h2>

<table>
  <thead>
    <tr>
      <th>对比项</th>
      <th>C++ 包（<code class="language-plaintext highlighter-rouge">ament_cmake</code>）</th>
      <th>Python 包（<code class="language-plaintext highlighter-rouge">ament_python</code>）</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>构建工具</td>
      <td>CMake（<code class="language-plaintext highlighter-rouge">CMakeLists.txt</code>）</td>
      <td>setuptools（<code class="language-plaintext highlighter-rouge">setup.py</code>）</td>
    </tr>
    <tr>
      <td>源码组织</td>
      <td><code class="language-plaintext highlighter-rouge">src/*.cpp</code> + <code class="language-plaintext highlighter-rouge">include/&lt;包名&gt;/*.hpp</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;包名&gt;/*.py</code>（同名包目录）</td>
    </tr>
    <tr>
      <td>可执行文件来源</td>
      <td><code class="language-plaintext highlighter-rouge">add_executable()</code> 编译产物</td>
      <td><code class="language-plaintext highlighter-rouge">console_scripts</code> 入口点</td>
    </tr>
    <tr>
      <td>ROS 客户端库</td>
      <td><code class="language-plaintext highlighter-rouge">rclcpp</code></td>
      <td><code class="language-plaintext highlighter-rouge">rclpy</code></td>
    </tr>
    <tr>
      <td>需要编译</td>
      <td>是（慢，但运行性能好）</td>
      <td>否（即改即用，无需重新构建）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ros2 run</code> 依赖</td>
      <td><code class="language-plaintext highlighter-rouge">install(TARGETS ...)</code></td>
      <td><code class="language-plaintext highlighter-rouge">console_scripts</code></td>
    </tr>
    <tr>
      <td>launch 安装规则</td>
      <td><code class="language-plaintext highlighter-rouge">install(DIRECTORY launch ...)</code></td>
      <td><code class="language-plaintext highlighter-rouge">setup.py</code> 的 <code class="language-plaintext highlighter-rouge">data_files</code></td>
    </tr>
    <tr>
      <td>适合场景</td>
      <td>对性能/实时性要求高的底层驱动、控制器</td>
      <td>快速原型、逻辑复杂、脚本化工具、launch 文件</td>
    </tr>
    <tr>
      <td>调试速度</td>
      <td>改动需重新 <code class="language-plaintext highlighter-rouge">colcon build</code></td>
      <td>改完直接运行，无需构建</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="四快速参考两个命令对比">四、快速参考：两个命令对比</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 创建 Python 包（带一个节点）</span>
ros2 pkg create my_py_pkg <span class="nt">--build-type</span> ament_python <span class="nt">--node-name</span> my_node

<span class="c"># 创建 C++ 包（带一个节点）</span>
ros2 pkg create my_cpp_pkg <span class="nt">--build-type</span> ament_cmake <span class="nt">--node-name</span> my_node
</code></pre></div></div>

<blockquote>
  <p><strong>提醒</strong>：<code class="language-plaintext highlighter-rouge">--node-name</code> 在 Python 包里会生成 <code class="language-plaintext highlighter-rouge">&lt;包名&gt;/&lt;节点名&gt;.py</code>，在 C++ 包里生成 <code class="language-plaintext highlighter-rouge">src/&lt;节点名&gt;.cpp</code>。创建完后记得：</p>
  <ul>
    <li>Python 包：去 <code class="language-plaintext highlighter-rouge">setup.py</code> 加 launch 安装规则（如需要 launch 文件）</li>
    <li>C++ 包：去 <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> 补 <code class="language-plaintext highlighter-rouge">install()</code> 三件套（如需要 launch/config 文件）</li>
  </ul>
</blockquote>]]></content><author><name>老张同志</name></author><category term="ros2" /><category term="tutorial" /><summary type="html"><![CDATA[从零掌握 ROS 2 功能包：分别介绍 C++ 功能包（ament_cmake）和 Python 功能包（ament_python）的目录结构、每个文件的用途与内容详解（CMakeLists.txt / setup.py / package.xml），以及创建、构建、运行的完整命令，附两类包对比总结表。]]></summary></entry><entry><title type="html">ROS 2 发布者与订阅者 —— 用 Python 从零实现最小 Pub/Sub</title><link href="https://1q08.github.io/ros_ws/ros2/tutorial/2026/08/11/ros2-pubsub-python.html" rel="alternate" type="text/html" title="ROS 2 发布者与订阅者 —— 用 Python 从零实现最小 Pub/Sub" /><published>2026-08-11T02:00:00+00:00</published><updated>2026-08-11T02:00:00+00:00</updated><id>https://1q08.github.io/ros_ws/ros2/tutorial/2026/08/11/ros2-pubsub-python</id><content type="html" xml:base="https://1q08.github.io/ros_ws/ros2/tutorial/2026/08/11/ros2-pubsub-python.html"><![CDATA[<h1 id="ros-2-发布者与订阅者publisher--subscriber">ROS 2 发布者与订阅者（Publisher &amp; Subscriber）</h1>

<blockquote>
  <p>ROS 2 中节点间通信最基础、最核心的模式就是 <strong>发布者 / 订阅者（Pub/Sub）</strong>。本文带你理解它的原理，并用 Python 从零写出一个最小可运行的发布者和订阅者。</p>
</blockquote>

<hr />

<h2 id="一什么是发布者与订阅者">一、什么是发布者与订阅者？</h2>

<p>在 ROS 2 中，一个可执行程序被称为 <strong>节点（Node）</strong>。节点之间通过 <strong>话题（Topic）</strong> 进行<strong>异步</strong>通信：</p>

<ul>
  <li><strong>发布者（Publisher）</strong>：向某个话题发送数据（消息）。</li>
  <li><strong>订阅者（Subscriber）</strong>：从某个话题接收数据（消息）。</li>
</ul>

<pre><code class="language-mermaid">flowchart LR
    A[发布者节点&lt;br/&gt;talker] --&gt;|"话题 /chatter&lt;br/&gt;std_msgs/String"| B[订阅者节点&lt;br/&gt;listener]
</code></pre>

<p>这种模式的几个关键特点：</p>

<table>
  <thead>
    <tr>
      <th>特点</th>
      <th>说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>解耦</strong></td>
      <td>发布者不关心谁在订阅，订阅者也不关心谁在发布，双方只通过话题名称和消息类型相连</td>
    </tr>
    <tr>
      <td><strong>一对多 / 多对一</strong></td>
      <td>一个话题可以有多个发布者、多个订阅者</td>
    </tr>
    <tr>
      <td><strong>异步</strong></td>
      <td>发布者发完即走，不等待订阅者处理完毕</td>
    </tr>
    <tr>
      <td><strong>无连接配置</strong></td>
      <td>节点无需互相知道对方的地址，由 DDS 中间件自动完成发现与传输</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>打个比方：<strong>话题就像广播电台的频率，消息就像广播内容</strong>。电台（发布者）只管播，听众（订阅者）打开对应频率就能收到，双方互不认识。</p>
</blockquote>

<hr />

<h2 id="二准备工作">二、准备工作</h2>

<p>本文基于 <strong>ROS 2 Jazzy + Python 3</strong>，假设你的环境已经配置好：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 检查 ROS 2 是否可用</span>
<span class="nb">printenv </span>ROS_DISTRO        <span class="c"># 应输出 jazzy</span>

<span class="c"># 每次打开终端都要 source 环境（也可写入 ~/.bashrc）</span>
<span class="nb">source</span> /opt/ros/jazzy/setup.bash
<span class="nb">source</span> ~/ros_ws/install/setup.bash
</code></pre></div></div>

<hr />

<h2 id="三最小代码样例">三、最小代码样例</h2>

<p>下面是最精简的发布者 / 订阅者，使用标准库消息类型 <code class="language-plaintext highlighter-rouge">std_msgs/msg/String</code>，话题名为 <code class="language-plaintext highlighter-rouge">chatter</code>。</p>

<h3 id="31-发布者-minimal_publisherpy">3.1 发布者 <code class="language-plaintext highlighter-rouge">minimal_publisher.py</code></h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">rclpy</span>
<span class="kn">from</span> <span class="nn">rclpy.node</span> <span class="kn">import</span> <span class="n">Node</span>
<span class="kn">from</span> <span class="nn">std_msgs.msg</span> <span class="kn">import</span> <span class="n">String</span>


<span class="k">class</span> <span class="nc">MinimalPublisher</span><span class="p">(</span><span class="n">Node</span><span class="p">):</span>
    <span class="s">"""每隔 0.5 秒向话题 chatter 发布一条字符串消息"""</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="s">'minimal_publisher'</span><span class="p">)</span>          <span class="c1"># 节点名称
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">publisher_</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">create_publisher</span><span class="p">(</span>       <span class="c1"># 创建发布者
</span>            <span class="n">String</span><span class="p">,</span>        <span class="c1"># 消息类型
</span>            <span class="s">'chatter'</span><span class="p">,</span>     <span class="c1"># 话题名称
</span>            <span class="mi">10</span><span class="p">)</span>            <span class="c1"># 队列长度（缓存 10 条）
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">timer</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">create_timer</span><span class="p">(</span><span class="mf">0.5</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">timer_callback</span><span class="p">)</span>  <span class="c1"># 定时器
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span>

    <span class="k">def</span> <span class="nf">timer_callback</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="n">msg</span> <span class="o">=</span> <span class="n">String</span><span class="p">()</span>
        <span class="n">msg</span><span class="p">.</span><span class="n">data</span> <span class="o">=</span> <span class="sa">f</span><span class="s">'Hello World: </span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="n">i</span><span class="si">}</span><span class="s">'</span>            <span class="c1"># 填充消息内容
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">publisher_</span><span class="p">.</span><span class="n">publish</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span>                   <span class="c1"># 发布
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">get_logger</span><span class="p">().</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s">'Publishing: "</span><span class="si">{</span><span class="n">msg</span><span class="p">.</span><span class="n">data</span><span class="si">}</span><span class="s">"'</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">i</span> <span class="o">+=</span> <span class="mi">1</span>


<span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">init</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="n">args</span><span class="p">)</span>                 <span class="c1"># 1. 初始化 rclpy
</span>    <span class="n">node</span> <span class="o">=</span> <span class="n">MinimalPublisher</span><span class="p">()</span>             <span class="c1"># 2. 创建节点
</span>    <span class="n">rclpy</span><span class="p">.</span><span class="n">spin</span><span class="p">(</span><span class="n">node</span><span class="p">)</span>                      <span class="c1"># 3. 让节点保持运行、处理回调
</span>    <span class="n">node</span><span class="p">.</span><span class="n">destroy_node</span><span class="p">()</span>                   <span class="c1"># 4. 清理
</span>    <span class="n">rclpy</span><span class="p">.</span><span class="n">shutdown</span><span class="p">()</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="32-订阅者-minimal_subscriberpy">3.2 订阅者 <code class="language-plaintext highlighter-rouge">minimal_subscriber.py</code></h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">rclpy</span>
<span class="kn">from</span> <span class="nn">rclpy.node</span> <span class="kn">import</span> <span class="n">Node</span>
<span class="kn">from</span> <span class="nn">std_msgs.msg</span> <span class="kn">import</span> <span class="n">String</span>


<span class="k">class</span> <span class="nc">MinimalSubscriber</span><span class="p">(</span><span class="n">Node</span><span class="p">):</span>
    <span class="s">"""订阅话题 chatter，每收到一条消息就打印出来"""</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="s">'minimal_subscriber'</span><span class="p">)</span>         <span class="c1"># 节点名称
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">subscription</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">create_subscription</span><span class="p">(</span>  <span class="c1"># 创建订阅者
</span>            <span class="n">String</span><span class="p">,</span>                  <span class="c1"># 消息类型
</span>            <span class="s">'chatter'</span><span class="p">,</span>               <span class="c1"># 话题名称
</span>            <span class="bp">self</span><span class="p">.</span><span class="n">listener_callback</span><span class="p">,</span>  <span class="c1"># 收到消息时调用的回调
</span>            <span class="mi">10</span><span class="p">)</span>                      <span class="c1"># 队列长度
</span>
    <span class="k">def</span> <span class="nf">listener_callback</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">msg</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">get_logger</span><span class="p">().</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s">'I heard: "</span><span class="si">{</span><span class="n">msg</span><span class="p">.</span><span class="n">data</span><span class="si">}</span><span class="s">"'</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">init</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="n">args</span><span class="p">)</span>
    <span class="n">node</span> <span class="o">=</span> <span class="n">MinimalSubscriber</span><span class="p">()</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">spin</span><span class="p">(</span><span class="n">node</span><span class="p">)</span>
    <span class="n">node</span><span class="p">.</span><span class="n">destroy_node</span><span class="p">()</span>
    <span class="n">rclpy</span><span class="p">.</span><span class="n">shutdown</span><span class="p">()</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="33-代码要点解读">3.3 代码要点解读</h3>

<table>
  <thead>
    <tr>
      <th>代码</th>
      <th>作用</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">rclpy.init()</code></td>
      <td>初始化 ROS 2 客户端库，<strong>每个进程必须调用一次</strong></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Node('名称')</code></td>
      <td>创建节点，节点名在整个 ROS 图中需唯一</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">create_publisher(Type, topic, qos)</code></td>
      <td>创建发布者：消息类型 / 话题名 / 队列深度</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">create_subscription(Type, topic, cb, qos)</code></td>
      <td>创建订阅者：收到消息后自动调用回调</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">create_timer(秒, 回调)</code></td>
      <td>定时触发回调，实现周期性发布</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">rclpy.spin(node)</code></td>
      <td>阻塞运行节点，持续处理消息与定时器回调</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">publish(msg)</code></td>
      <td>把消息发送到话题上</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>队列长度（QoS 深度）</strong>：订阅者处理较慢时，最多在本地缓存这么多条待处理消息，超出后丢弃最旧的。生产环境中应根据数据的重要程度合理设置。</p>
</blockquote>

<hr />

<h2 id="四完整过程从包到运行">四、完整过程（从包到运行）</h2>

<h3 id="第-1-步创建功能包">第 1 步：创建功能包</h3>

<p>在 <code class="language-plaintext highlighter-rouge">src/</code> 下用官方命令创建 Python 包（这里包名用 <code class="language-plaintext highlighter-rouge">py_pubsub</code> 演示，也可换成你自己的名字）：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/ros_ws/src
ros2 pkg create py_pubsub <span class="nt">--build-type</span> ament_python <span class="nt">--node-name</span> publisher
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--node-name publisher</code> 会自动生成 <code class="language-plaintext highlighter-rouge">py_pubsub/publisher.py</code> 并在 <code class="language-plaintext highlighter-rouge">setup.py</code> 中注册入口。</p>

<h3 id="第-2-步放置代码文件">第 2 步：放置代码文件</h3>

<p>把上面两段代码分别保存为：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src/py_pubsub/py_pubsub/publisher.py     # 发布者
src/py_pubsub/py_pubsub/subscriber.py    # 订阅者
</code></pre></div></div>

<p>（也可以直接使用官方样例生成的 <code class="language-plaintext highlighter-rouge">publisher_member_function.py</code> / <code class="language-plaintext highlighter-rouge">subscriber_member_function.py</code>，本文为你手写的是更精简的版本。）</p>

<h3 id="第-3-步配置-setuppy">第 3 步：配置 <code class="language-plaintext highlighter-rouge">setup.py</code></h3>

<p>编辑 <code class="language-plaintext highlighter-rouge">src/py_pubsub/setup.py</code>，在 <code class="language-plaintext highlighter-rouge">console_scripts</code> 中注册两个可执行入口：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">entry_points</span><span class="o">=</span><span class="p">{</span>
    <span class="s">'console_scripts'</span><span class="p">:</span> <span class="p">[</span>
        <span class="s">'publisher = py_pubsub.publisher:main'</span><span class="p">,</span>
        <span class="s">'subscriber = py_pubsub.subscriber:main'</span><span class="p">,</span>
    <span class="p">],</span>
<span class="p">},</span>
</code></pre></div></div>

<p>格式为：<code class="language-plaintext highlighter-rouge">命令名 = 模块路径:函数名</code>。这样构建后就能用 <code class="language-plaintext highlighter-rouge">ros2 run py_pubsub publisher</code> 直接启动。</p>

<h3 id="第-4-步确认-packagexml-依赖">第 4 步：确认 <code class="language-plaintext highlighter-rouge">package.xml</code> 依赖</h3>

<p>确保 <code class="language-plaintext highlighter-rouge">package.xml</code> 里声明了 <code class="language-plaintext highlighter-rouge">rclpy</code> 和 <code class="language-plaintext highlighter-rouge">std_msgs</code>（<code class="language-plaintext highlighter-rouge">ros2 pkg create</code> 默认已带，可不用改）：</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;exec_depend&gt;</span>rclpy<span class="nt">&lt;/exec_depend&gt;</span>
<span class="nt">&lt;exec_depend&gt;</span>std_msgs<span class="nt">&lt;/exec_depend&gt;</span>
</code></pre></div></div>

<h3 id="第-5-步构建">第 5 步：构建</h3>

<p>回到工作区根目录，构建这个包（<strong>每次改代码后都要重新构建</strong>）：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/ros_ws
colcon build <span class="nt">--packages-select</span> py_pubsub
<span class="nb">source install</span>/setup.bash
</code></pre></div></div>

<h3 id="第-6-步运行">第 6 步：运行</h3>

<p>开<strong>两个终端</strong>，分别运行发布者和订阅者：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 终端 1：发布者</span>
<span class="nb">source</span> /opt/ros/jazzy/setup.bash
<span class="nb">source</span> ~/ros_ws/install/setup.bash
ros2 run py_pubsub publisher
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 终端 2：订阅者</span>
<span class="nb">source</span> /opt/ros/jazzy/setup.bash
<span class="nb">source</span> ~/ros_ws/install/setup.bash
ros2 run py_pubsub subscriber
</code></pre></div></div>

<h3 id="第-7-步验证结果">第 7 步：验证结果</h3>

<ul>
  <li><strong>订阅者终端</strong>会不断打印：<code class="language-plaintext highlighter-rouge">I heard: "Hello World: 0"</code>、<code class="language-plaintext highlighter-rouge">I heard: "Hello World: 1"</code>……</li>
  <li>另开一个终端可以查看话题与节点信息：</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ros2 topic list          <span class="c"># 查看所有话题，应包含 /chatter</span>
ros2 topic <span class="nb">echo</span> /chatter <span class="c"># 实时打印话题上的消息</span>
ros2 node list           <span class="c"># 查看节点：/minimal_publisher /minimal_subscriber</span>
</code></pre></div></div>

<p>预期输出示例：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ros2 topic echo /chatter
data: 'Hello World: 42'
---
data: 'Hello World: 43'
---
</code></pre></div></div>

<hr />

<h2 id="五常见问题排查">五、常见问题排查</h2>

<table>
  <thead>
    <tr>
      <th>现象</th>
      <th>原因 / 解决</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ros2 run</code> 提示找不到包</td>
      <td>没 source 工作区：<code class="language-plaintext highlighter-rouge">source ~/ros_ws/install/setup.bash</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ros2 run</code> 提示找不到可执行文件</td>
      <td><code class="language-plaintext highlighter-rouge">setup.py</code> 的 <code class="language-plaintext highlighter-rouge">console_scripts</code> 没注册，或改后没重新 <code class="language-plaintext highlighter-rouge">colcon build</code></td>
    </tr>
    <tr>
      <td>订阅者收不到消息</td>
      <td>话题名不一致（两边必须都是 <code class="language-plaintext highlighter-rouge">chatter</code>）；或节点处于不同 <code class="language-plaintext highlighter-rouge">ROS_DOMAIN_ID</code></td>
    </tr>
    <tr>
      <td>改了代码但行为没变</td>
      <td>忘记重新构建：<code class="language-plaintext highlighter-rouge">colcon build --packages-select py_pubsub</code></td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="六小结">六、小结</h2>

<ul>
  <li><strong>发布者/订阅者</strong>是 ROS 2 最基础的异步通信模式，通过<strong>话题 + 消息类型</strong>解耦。</li>
  <li>用 Python 只需掌握 <code class="language-plaintext highlighter-rouge">rclpy.init()</code>、<code class="language-plaintext highlighter-rouge">Node</code>、<code class="language-plaintext highlighter-rouge">create_publisher</code> / <code class="language-plaintext highlighter-rouge">create_subscription</code>、<code class="language-plaintext highlighter-rouge">rclpy.spin()</code> 五个关键点。</li>
  <li>完整流程：<strong>建包 → 写代码 → 注册入口 → 构建 → 运行</strong>，每一步缺一不可。</li>
</ul>

<p>掌握了 Pub/Sub，你就打通了 ROS 2 程序间通信的大门，后面的服务（Service）、动作（Action）都是在此基础上的扩展。</p>]]></content><author><name>老张同志</name></author><category term="ros2" /><category term="tutorial" /><summary type="html"><![CDATA[理解 ROS 2 最基础的发布者/订阅者（Pub/Sub）通信模式，并用 Python 从零写出最小可运行的 talker 与 listener，附建包、构建、运行全流程。]]></summary></entry><entry><title type="html">ROS 多版本管理 —— 一台机器跑多个 ROS 的完整方案</title><link href="https://1q08.github.io/ros_ws/ros2/tutorial/2026/08/11/ros-version-management.html" rel="alternate" type="text/html" title="ROS 多版本管理 —— 一台机器跑多个 ROS 的完整方案" /><published>2026-08-11T01:00:00+00:00</published><updated>2026-08-11T01:00:00+00:00</updated><id>https://1q08.github.io/ros_ws/ros2/tutorial/2026/08/11/ros-version-management</id><content type="html" xml:base="https://1q08.github.io/ros_ws/ros2/tutorial/2026/08/11/ros-version-management.html"><![CDATA[<h1 id="ros-多版本管理--一台机器跑多个-ros-的完整方案">ROS 多版本管理 —— 一台机器跑多个 ROS 的完整方案</h1>

<blockquote>
  <p><strong>适用场景</strong>：团队 / 个人同时维护 ROS 1 与 ROS 2 项目，或需要运行不同发行版（如 Humble 与 Jazzy）的 ROS 2 项目，但只有一台开发机。</p>
</blockquote>

<p><strong>核心结论先行</strong>：</p>

<ul>
  <li><strong>ROS 1 与 ROS 2 可以装在同一台机器上</strong>，通过 <code class="language-plaintext highlighter-rouge">source</code> 不同的 setup 文件切换使用。</li>
  <li><strong>ROS 2 不同发行版（Humble / Jazzy）不建议直接共存在宿主机</strong>，推荐用 <strong>Docker 容器</strong> 隔离。</li>
  <li>最干净、最可复现的通用方案是 <strong>Docker + 容器化开发</strong>，环境互不干扰，项目间一键切换。</li>
</ul>

<hr />

<h2 id="目录">目录</h2>

<ol>
  <li><a href="#一为什么-ros-版本会打架">为什么 ROS 版本会”打架”</a></li>
  <li><a href="#二ros-发行版与-ubuntu-的对应关系">ROS 发行版与 Ubuntu 的对应关系</a></li>
  <li><a href="#三关键概念ros-环境变量体系">关键概念：ROS 环境变量体系</a></li>
  <li><a href="#四方案一环境切换法ros1--ros2-共存">方案一：环境切换法（ROS 1 + ROS 2 共存）</a></li>
  <li><a href="#五方案二docker-容器化推荐支持任意版本组合">方案二：Docker 容器化（推荐，支持任意版本组合）</a></li>
  <li><a href="#六方案三python-工具链隔离">方案三：Python 工具链隔离</a></li>
  <li><a href="#七ros1-与-ros2-互通ros1_bridge">ROS 1 与 ROS 2 互通：ros1_bridge</a></li>
  <li><a href="#八日常检查与排查命令">日常检查与排查命令</a></li>
  <li><a href="#九方案对比总结">方案对比总结</a></li>
</ol>

<hr />

<h2 id="一为什么-ros-版本会打架">一、为什么 ROS 版本会”打架”</h2>

<p>ROS 通过<strong>环境变量</strong>来决定当前终端使用哪一套工具链和库。当你 <code class="language-plaintext highlighter-rouge">source</code> 一个发行版的 setup 文件时，实际修改了这些环境变量：</p>

<table>
  <thead>
    <tr>
      <th>环境变量</th>
      <th>作用</th>
      <th>示例（Jazzy）</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ROS_DISTRO</code></td>
      <td>当前 ROS 发行版标识</td>
      <td><code class="language-plaintext highlighter-rouge">jazzy</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ROS_VERSION</code></td>
      <td>ROS 大版本号</td>
      <td><code class="language-plaintext highlighter-rouge">2</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">AMENT_PREFIX_PATH</code></td>
      <td>ROS 2 包查找路径</td>
      <td><code class="language-plaintext highlighter-rouge">/opt/ros/jazzy</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CMAKE_PREFIX_PATH</code></td>
      <td>编译时依赖查找路径</td>
      <td><code class="language-plaintext highlighter-rouge">/opt/ros/jazzy</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PYTHONPATH</code></td>
      <td>Python 模块查找路径</td>
      <td><code class="language-plaintext highlighter-rouge">/opt/ros/jazzy/lib/python3.12/site-packages</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">LD_LIBRARY_PATH</code></td>
      <td>动态库查找路径</td>
      <td><code class="language-plaintext highlighter-rouge">/opt/ros/jazzy/lib</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PATH</code></td>
      <td>可执行文件查找路径</td>
      <td><code class="language-plaintext highlighter-rouge">/opt/ros/jazzy/bin</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">RMW_IMPLEMENTATION</code></td>
      <td>中间件实现（ROS 2）</td>
      <td><code class="language-plaintext highlighter-rouge">rmw_fastrtps_cpp</code></td>
    </tr>
  </tbody>
</table>

<p><strong>冲突的本质</strong>：</p>

<ul>
  <li>两个版本的 setup 文件<strong>不能同时 source</strong>，否则后面的会覆盖前面的 <code class="language-plaintext highlighter-rouge">PYTHONPATH</code> / <code class="language-plaintext highlighter-rouge">LD_LIBRARY_PATH</code> / <code class="language-plaintext highlighter-rouge">CMAKE_PREFIX_PATH</code>，导致找错库、找错包。</li>
  <li>ROS 1（Noetic）是单进程架构；ROS 2 是 DDS 分布式架构，两者通信协议完全不同。</li>
  <li>ROS 2 各发行版的<strong>系统级 Python 工具</strong>（<code class="language-plaintext highlighter-rouge">colcon</code>、<code class="language-plaintext highlighter-rouge">rosdep</code>、<code class="language-plaintext highlighter-rouge">vcs</code>）通过 apt 安装在同一位置，Humble 和 Jazzy 会互相覆盖，这是它们难以在宿主机共存的直接原因。</li>
</ul>

<hr />

<h2 id="二ros-发行版与-ubuntu-的对应关系">二、ROS 发行版与 Ubuntu 的对应关系</h2>

<table>
  <thead>
    <tr>
      <th>ROS 版本</th>
      <th>类型</th>
      <th>官方支持的系统</th>
      <th>Python</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Noetic</strong></td>
      <td>ROS 1</td>
      <td>Ubuntu 20.04 (Focal)</td>
      <td>Python 3.8</td>
    </tr>
    <tr>
      <td><strong>Foxy</strong></td>
      <td>ROS 2</td>
      <td>Ubuntu 20.04 (Focal)</td>
      <td>Python 3.8</td>
    </tr>
    <tr>
      <td><strong>Humble</strong></td>
      <td>ROS 2 LTS</td>
      <td>Ubuntu 22.04 (Jammy)</td>
      <td>Python 3.10</td>
    </tr>
    <tr>
      <td><strong>Iron</strong></td>
      <td>ROS 2</td>
      <td>Ubuntu 22.04 (Jammy)</td>
      <td>Python 3.10</td>
    </tr>
    <tr>
      <td><strong>Jazzy</strong></td>
      <td>ROS 2 LTS</td>
      <td>Ubuntu 24.04 (Noble)</td>
      <td>Python 3.12</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>关键点</strong>：每个 ROS 发行版与特定 Ubuntu 版本、特定 Python 版本强绑定。
一台宿主机的 Ubuntu 版本一旦固定，能直接通过 apt 安装的 ROS 2 发行版基本只有一个。
想要运行其它发行版，要么<strong>源码编译</strong>（费时且易出问题），要么<strong>用 Docker 跑对应系统的容器</strong>（推荐）。</p>
</blockquote>

<hr />

<h2 id="三关键概念ros-环境变量体系">三、关键概念：ROS 环境变量体系</h2>

<p>每个 ROS 环境都有各自的 <code class="language-plaintext highlighter-rouge">setup</code> 文件：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/opt/ros/&lt;distro&gt;/setup.bash        # ROS 1 和 ROS 2 通用入口
/opt/ros/&lt;distro&gt;/local_setup.bash  # 当前工作空间安装的环境
</code></pre></div></div>

<p>切换到某个 ROS 版本，本质就是在终端里执行对应的 <code class="language-plaintext highlighter-rouge">source</code>。<strong>不同终端相互独立</strong>，因此：</p>

<ul>
  <li>终端 A source 了 Jazzy，终端 B source 了 Humble，两者互不影响。</li>
  <li>同一终端内反复 source 不同版本，会残留/污染环境，建议<strong>开新终端</strong>再切换。</li>
</ul>

<p>检查当前环境的黄金命令：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="nv">$ROS_DISTRO</span>        <span class="c"># 显示当前发行版</span>
<span class="nb">printenv</span> | <span class="nb">grep</span> <span class="nt">-E</span> <span class="s1">'ROS|AMENT|COLCON'</span>   <span class="c"># 查看全部相关环境变量</span>
</code></pre></div></div>

<hr />

<h2 id="四方案一环境切换法ros-1--ros-2-共存">四、方案一：环境切换法（ROS 1 + ROS 2 共存）</h2>

<blockquote>
  <p>适用于 <strong>ROS 1 与 ROS 2 在同一 Ubuntu 上共存</strong>（例如 Ubuntu 20.04 同时装 Noetic 与 Foxy），
因为 ROS 1 与 ROS 2 的安装路径、工具链基本不重叠，可以真正装在同一台宿主机上。</p>
</blockquote>

<h3 id="41-安装">4.1 安装</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 安装 ROS 1 Noetic（Ubuntu 20.04）</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>ros-noetic-desktop

<span class="c"># 安装 ROS 2 Foxy（Ubuntu 20.04，可与 Noetic 共存）</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>ros-foxy-desktop
</code></pre></div></div>

<blockquote>
  <p>注意：如果你在 Ubuntu 22.04/24.04 上想同时用 ROS 1 与 ROS 2，ROS 1 Noetic 没有官方二进制包，
需要<strong>源码编译</strong>或使用容器，因此更推荐直接跳到 Docker 方案。</p>
</blockquote>

<h3 id="42-在-bashrc-中配置快速切换">4.2 在 <code class="language-plaintext highlighter-rouge">.bashrc</code> 中配置快速切换</h3>

<p>不要在 <code class="language-plaintext highlighter-rouge">.bashrc</code> 里同时 <code class="language-plaintext highlighter-rouge">source</code> 两个版本，而是定义切换函数，按需调用：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 编辑 ~/.bashrc</span>
<span class="nb">cat</span> <span class="o">&gt;&gt;</span> ~/.bashrc <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">EOF</span><span class="sh">'

# ===== ROS 版本切换 =====
export ROS_WS=~/ros_ws   # 你的工作空间路径

use_ros1() {
    # 若当前已 source 过 ROS 2，建议开新终端或先执行 env -i bash
    source /opt/ros/noetic/setup.bash
    if [ -f "</span><span class="nv">$ROS_WS</span><span class="sh">/devel/setup.bash" ]; then
        source "</span><span class="nv">$ROS_WS</span><span class="sh">/devel/setup.bash"    # ROS 1 使用 catkin 生成 devel
    fi
    echo "&gt;&gt;&gt; 已切换到 ROS 1 (Noetic)"
}

use_ros2() {
    source /opt/ros/foxy/setup.bash
    if [ -f "</span><span class="nv">$ROS_WS</span><span class="sh">/install/setup.bash" ]; then
        source "</span><span class="nv">$ROS_WS</span><span class="sh">/install/setup.bash"  # ROS 2 使用 colcon 生成 install
    fi
    echo "&gt;&gt;&gt; 已切换到 ROS 2 (Foxy)"
}
</span><span class="no">EOF
</span><span class="nb">source</span> ~/.bashrc
</code></pre></div></div>

<h3 id="43-使用方式">4.3 使用方式</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>use_ros1 <span class="o">&amp;&amp;</span> roscore &amp;            <span class="c"># 运行 ROS 1 master</span>
rosrun turtlesim turtlesim_node  <span class="c"># 运行 ROS 1 节点</span>

<span class="c"># ---------- 另开一个终端 ----------</span>
use_ros2 <span class="o">&amp;&amp;</span> ros2 run turtlesim turtlesim_node   <span class="c"># 运行 ROS 2 节点</span>
</code></pre></div></div>

<h3 id="44-重要注意事项">4.4 重要注意事项</h3>

<ul>
  <li><strong>不要</strong>同时 <code class="language-plaintext highlighter-rouge">source</code> ROS 1 与 ROS 2 的 setup 文件，<code class="language-plaintext highlighter-rouge">PYTHONPATH</code> / <code class="language-plaintext highlighter-rouge">LD_LIBRARY_PATH</code> 会互相污染。</li>
  <li>ROS 1 命令（<code class="language-plaintext highlighter-rouge">rospack</code>、<code class="language-plaintext highlighter-rouge">rosrun</code>、<code class="language-plaintext highlighter-rouge">catkin_make</code>）与 ROS 2 命令（<code class="language-plaintext highlighter-rouge">ros2</code>、<code class="language-plaintext highlighter-rouge">colcon</code>）分属不同工具链。</li>
  <li>同一终端内切换版本后，最好 <code class="language-plaintext highlighter-rouge">echo $ROS_DISTRO</code> 确认，必要时直接开新终端。</li>
</ul>

<hr />

<h2 id="五方案二docker-容器化推荐支持任意版本组合">五、方案二：Docker 容器化（推荐，支持任意版本组合）</h2>

<blockquote>
  <p><strong>为什么最推荐</strong>：每个 ROS 发行版对应不同的 Ubuntu/Python，宿主机只能装一个；
Docker 把”操作系统 + ROS 版本 + 依赖”整体打包，彻底解决所有冲突，且可复现、可分发。</p>
</blockquote>

<h3 id="51-常用官方镜像">5.1 常用官方镜像</h3>

<table>
  <thead>
    <tr>
      <th>需求</th>
      <th>镜像</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ROS 1 Noetic</td>
      <td><code class="language-plaintext highlighter-rouge">ros:noetic-ros-base</code> / <code class="language-plaintext highlighter-rouge">ros:noetic-ros-core</code></td>
    </tr>
    <tr>
      <td>ROS 2 Humble</td>
      <td><code class="language-plaintext highlighter-rouge">osrf/ros:humble-desktop</code> / <code class="language-plaintext highlighter-rouge">osrf/ros:humble-ros-base</code></td>
    </tr>
    <tr>
      <td>ROS 2 Jazzy</td>
      <td><code class="language-plaintext highlighter-rouge">osrf/ros:jazzy-desktop</code> / <code class="language-plaintext highlighter-rouge">osrf/ros:jazzy-ros-base</code></td>
    </tr>
    <tr>
      <td>ROS 2 Foxy</td>
      <td><code class="language-plaintext highlighter-rouge">osrf/ros:foxy-desktop</code></td>
    </tr>
  </tbody>
</table>

<h3 id="52-基本运行命令带-gui-与共享目录">5.2 基本运行命令（带 GUI 与共享目录）</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 允许 X11 转发（宿主机执行一次即可）</span>
xhost +local:docker

<span class="c"># 运行 Humble 容器</span>
docker run <span class="nt">-it</span> <span class="nt">--rm</span> <span class="se">\</span>
  <span class="nt">--name</span> ros_humble <span class="se">\</span>
  <span class="nt">--network</span> host <span class="se">\</span>
  <span class="nt">-e</span> <span class="nv">DISPLAY</span><span class="o">=</span><span class="nv">$DISPLAY</span> <span class="se">\</span>
  <span class="nt">-e</span> <span class="nv">LIBGL_ALWAYS_SOFTWARE</span><span class="o">=</span>1 <span class="se">\</span>
  <span class="nt">-v</span> /tmp/.X11-unix:/tmp/.X11-unix <span class="se">\</span>
  <span class="nt">-v</span> ~/humble_ws:/root/ws <span class="se">\ </span>       <span class="c"># 挂载项目目录</span>
  <span class="nt">-v</span> /dev:/dev <span class="nt">--privileged</span> <span class="se">\ </span>     <span class="c"># 访问 USB / 串口等硬件（按需）</span>
  osrf/ros:humble-desktop
</code></pre></div></div>

<blockquote>
  <p><strong>GUI 透传说明</strong>（详细方案见 <strong><a href="#55-运行仿真图形界面gui完整解决方案">5.5 运行仿真：图形界面（GUI）完整解决方案</a></strong>）：</p>
  <ul>
    <li>X11：<code class="language-plaintext highlighter-rouge">-e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix</code></li>
    <li>Wayland（新 Ubuntu）：<code class="language-plaintext highlighter-rouge">-e WAYLAND_DISPLAY=$WAYLAND_DISPLAY -v $XDG_RUNTIME_DIR:/tmp/runtime-$USER -e XDG_RUNTIME_DIR=/tmp/runtime-$USER</code></li>
    <li>若 RViz/Gazebo 黑屏或闪退，加 <code class="language-plaintext highlighter-rouge">-e LIBGL_ALWAYS_SOFTWARE=1</code>（软件渲染）。</li>
  </ul>
</blockquote>

<h3 id="53-在容器内构建与运行">5.3 在容器内构建与运行</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 进入容器后（bash 交互）</span>
<span class="nb">source</span> /opt/ros/humble/setup.bash
<span class="nb">cd</span> /root/ws
colcon build <span class="nt">--symlink-install</span>
<span class="nb">source install</span>/setup.bash
ros2 launch my_pkg my_launch.launch.py
</code></pre></div></div>

<h3 id="54-用-docker-compose-管理多版本项目推荐">5.4 用 docker-compose 管理多版本项目（推荐）</h3>

<p>写一个 <code class="language-plaintext highlighter-rouge">docker-compose.yml</code>，为每个项目定义独立容器：</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3.8"</span>

<span class="na">services</span><span class="pi">:</span>
  <span class="na">humble</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">osrf/ros:humble-desktop</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">ros_humble_dev</span>
    <span class="na">network_mode</span><span class="pi">:</span> <span class="s">host</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">DISPLAY=${DISPLAY}</span>
      <span class="pi">-</span> <span class="s">LIBGL_ALWAYS_SOFTWARE=1</span>
      <span class="pi">-</span> <span class="s">ROS_DOMAIN_ID=42</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/tmp/.X11-unix:/tmp/.X11-unix</span>
      <span class="pi">-</span> <span class="s">./humble_ws:/root/ws</span>
    <span class="na">stdin_open</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">tty</span><span class="pi">:</span> <span class="no">true</span>

  <span class="na">jazzy</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">osrf/ros:jazzy-desktop</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">ros_jazzy_dev</span>
    <span class="na">network_mode</span><span class="pi">:</span> <span class="s">host</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">DISPLAY=${DISPLAY}</span>
      <span class="pi">-</span> <span class="s">ROS_DOMAIN_ID=43</span>   <span class="c1"># 不同域，避免与 humble 容器互相发现</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/tmp/.X11-unix:/tmp/.X11-unix</span>
      <span class="pi">-</span> <span class="s">./jazzy_ws:/root/ws</span>
    <span class="na">stdin_open</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">tty</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose up <span class="nt">-d</span> humble       <span class="c"># 启动 Humble 容器</span>
docker compose up <span class="nt">-d</span> jazzy        <span class="c"># 启动 Jazzy 容器</span>
docker <span class="nb">exec</span> <span class="nt">-it</span> ros_humble_dev bash
docker <span class="nb">exec</span> <span class="nt">-it</span> ros_jazzy_dev bash
</code></pre></div></div>

<blockquote>
  <p><strong>Tips</strong>：</p>
  <ul>
    <li>给不同版本容器设置不同的 <code class="language-plaintext highlighter-rouge">ROS_DOMAIN_ID</code>，避免两者 DDS 互相发现。</li>
    <li>用 <code class="language-plaintext highlighter-rouge">--network host</code> 便于容器内与宿主机/其它容器通信。</li>
  </ul>
</blockquote>

<h3 id="55-运行仿真图形界面gui完整解决方案">5.5 运行仿真：图形界面（GUI）完整解决方案</h3>

<p><strong>核心问题</strong>：容器默认是”无头”（headless）的，没有显示器。要运行 Gazebo / RViz / rqt 等仿真 GUI，必须把图形环境「透传」进容器。根据<strong>宿主机是否有显示器</strong>、<strong>是否需要硬件加速</strong>，分为以下四种方案。</p>

<h4 id="551-方案-ax11-转发本地开发机最常用">5.5.1 方案 A：X11 转发（本地开发机，最常用）</h4>

<p>适用于宿主机<strong>自带显示器</strong>（桌面机 / 带屏的开发板）。原理：容器把窗口绘制请求转发给宿主机的 X Server 显示。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># ① 宿主机允许容器访问 X Server（每次重启后执行一次即可）</span>
xhost +local:docker
<span class="c"># 更精确写法：xhost +local:root   （因为容器内用户通常是 root）</span>

<span class="c"># ② 启动容器，带上 X11 透传参数</span>
docker run <span class="nt">-it</span> <span class="nt">--rm</span> <span class="se">\</span>
  <span class="nt">--name</span> ros_humble <span class="se">\</span>
  <span class="nt">--network</span> host <span class="se">\</span>
  <span class="nt">-e</span> <span class="nv">DISPLAY</span><span class="o">=</span><span class="nv">$DISPLAY</span> <span class="se">\</span>
  <span class="nt">-e</span> <span class="nv">QT_X11_NO_MITSHM</span><span class="o">=</span>1 <span class="se">\</span>
  <span class="nt">-e</span> <span class="nv">LIBGL_ALWAYS_SOFTWARE</span><span class="o">=</span>1 <span class="se">\</span>
  <span class="nt">-v</span> /tmp/.X11-unix:/tmp/.X11-unix <span class="se">\</span>
  <span class="nt">-v</span> ~/.Xauthority:/root/.Xauthority:rw <span class="se">\</span>
  <span class="nt">-v</span> ~/humble_ws:/root/ws <span class="se">\</span>
  osrf/ros:humble-desktop

<span class="c"># ③ 容器内启动仿真</span>
<span class="nb">source</span> /opt/ros/humble/setup.bash
gazebo       <span class="c"># 或 ros2 launch &lt;pkg&gt; &lt;launch&gt;.launch.py</span>
rviz2
</code></pre></div></div>

<p><strong>参数说明</strong>：</p>

<table>
  <thead>
    <tr>
      <th>参数</th>
      <th>作用</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-e DISPLAY=$DISPLAY</code></td>
      <td>把宿主机的显示编号（如 <code class="language-plaintext highlighter-rouge">:0</code>）传进容器</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-v /tmp/.X11-unix:/tmp/.X11-unix</code></td>
      <td>共享 X Server 的 Unix socket</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-v ~/.Xauthority:/root/.Xauthority:rw</code></td>
      <td>容器内以 root 运行需带授权文件，否则报 <code class="language-plaintext highlighter-rouge">No protocol specified</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-e QT_X11_NO_MITSHM=1</code></td>
      <td>解决 Qt 程序在容器内的共享内存报错</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-e LIBGL_ALWAYS_SOFTWARE=1</code></td>
      <td>软件渲染（mesa），无 GPU 驱动时防黑屏闪退（性能较差）</td>
    </tr>
  </tbody>
</table>

<h4 id="552-方案-bvnc-无头方案服务器--无显示器--远程访问">5.5.2 方案 B：VNC 无头方案（服务器 / 无显示器 / 远程访问）</h4>

<p>适用于<strong>没有显示器的服务器</strong>，或需要<strong>远程</strong>看仿真画面的场景。思路：在容器内跑一个虚拟显示器（Xvfb）+ 轻量桌面 + VNC 服务，通过 VNC 客户端或浏览器访问。</p>

<p><strong>① 在 Dockerfile 中预装 GUI + VNC</strong>（在基础 ROS 镜像上追加）：</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> osrf/ros:humble-desktop</span>
<span class="k">RUN </span>apt-get update <span class="o">&amp;&amp;</span> apt-get <span class="nb">install</span> <span class="nt">-y</span> <span class="se">\
</span>      xvfb x11vnc fluxbox xterm <span class="se">\
</span>      mesa-utils dbus-x11 <span class="se">\
</span>    <span class="o">&amp;&amp;</span> <span class="nb">mkdir</span> <span class="nt">-p</span> /root/.vnc <span class="se">\
</span>    <span class="o">&amp;&amp;</span> x11vnc <span class="nt">-storepasswd</span> 123456 /root/.vnc/passwd
<span class="k">ENV</span><span class="s"> DISPLAY=:1</span>
</code></pre></div></div>

<p><strong>② 启动容器并拉起 VNC 服务</strong>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-it</span> <span class="nt">--rm</span> <span class="se">\</span>
  <span class="nt">--name</span> ros_humble_gui <span class="se">\</span>
  <span class="nt">-p</span> 5900:5900 <span class="se">\</span>
  <span class="nt">-p</span> 6080:6080 <span class="se">\</span>
  osrf/ros:humble-desktop <span class="se">\</span>
  bash <span class="nt">-c</span> <span class="s2">"Xvfb :1 -screen 0 1600x900x24 &amp; </span><span class="se">\</span><span class="s2">
           fluxbox &amp; </span><span class="se">\</span><span class="s2">
           x11vnc -display :1 -forever -usepw -rfbport 5900 &amp; </span><span class="se">\</span><span class="s2">
           source /opt/ros/humble/setup.bash &amp;&amp; </span><span class="se">\</span><span class="s2">
           bash"</span>
</code></pre></div></div>

<p><strong>③ 连接方式</strong>（二选一）：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>方式一：VNC 客户端连接 &lt;宿主机IP&gt;:5900，密码 123456
方式二：容器内再装 noVNC，浏览器访问 http://&lt;宿主机IP&gt;:6080/vnc.html（无需客户端）
</code></pre></div></div>

<p>进入 VNC 桌面后，在里面打开终端执行 <code class="language-plaintext highlighter-rouge">gazebo</code> / <code class="language-plaintext highlighter-rouge">rviz2</code> 即可看到仿真画面。</p>

<h4 id="553-方案-cwayland-直通新-ubuntu-2404">5.5.3 方案 C：Wayland 直通（新 Ubuntu 24.04+）</h4>

<p>若宿主机登录时选择了 <strong>Wayland 会话</strong>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-it</span> <span class="nt">--rm</span> <span class="se">\</span>
  <span class="nt">--network</span> host <span class="se">\</span>
  <span class="nt">-e</span> <span class="nv">WAYLAND_DISPLAY</span><span class="o">=</span><span class="nv">$WAYLAND_DISPLAY</span> <span class="se">\</span>
  <span class="nt">-v</span> <span class="nv">$XDG_RUNTIME_DIR</span>:/tmp/runtime-<span class="nv">$USER</span> <span class="se">\</span>
  <span class="nt">-e</span> <span class="nv">XDG_RUNTIME_DIR</span><span class="o">=</span>/tmp/runtime-<span class="nv">$USER</span> <span class="se">\</span>
  ...
</code></pre></div></div>

<blockquote>
  <p><strong>实际经验</strong>：多数 ROS 工具（Gazebo / RViz / Qt 应用）是 <strong>X11 程序</strong>，在 Wayland 下通常通过 <strong>XWayland 兼容层</strong>运行，所以绝大多数场景直接用 <strong>方案 A 的 X11 转发</strong>即可。Wayland 原生直通主要用于少数纯 Wayland 客户端，不必强求。</p>
</blockquote>

<h4 id="554-方案-dgpu-硬件加速仿真流畅的关键">5.5.4 方案 D：GPU 硬件加速（仿真流畅的关键）</h4>

<p>Gazebo / RViz 是 3D 应用，<code class="language-plaintext highlighter-rouge">LIBGL_ALWAYS_SOFTWARE=1</code> 软件渲染会<strong>非常卡</strong>。要流畅仿真，必须把 GPU 透传给容器。</p>

<p><strong>① NVIDIA 独立显卡（x86 桌面机）</strong>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 宿主机安装 NVIDIA Container Toolkit（一次）</span>
<span class="nb">sudo </span>apt-get <span class="nb">install</span> <span class="nt">-y</span> nvidia-container-toolkit
<span class="nb">sudo </span>systemctl restart docker

<span class="c"># 启动容器时加 --gpus 参数</span>
docker run <span class="nt">-it</span> <span class="nt">--rm</span> <span class="nt">--gpus</span> all <span class="se">\</span>
  <span class="nt">--network</span> host <span class="se">\</span>
  <span class="nt">-e</span> <span class="nv">DISPLAY</span><span class="o">=</span><span class="nv">$DISPLAY</span> <span class="se">\</span>
  <span class="nt">-e</span> <span class="nv">NVIDIA_DRIVER_CAPABILITIES</span><span class="o">=</span>all <span class="se">\</span>
  <span class="nt">-v</span> /tmp/.X11-unix:/tmp/.X11-unix <span class="se">\</span>
  osrf/ros:humble-desktop
</code></pre></div></div>

<p><strong>② Jetson / ARM64 开发板（如你的设备）</strong>：</p>

<p>Jetson 的 GPU 依赖专用驱动（不能直接用 <code class="language-plaintext highlighter-rouge">--gpus all</code>），必须使用 <strong>NVIDIA L4T 基础镜像</strong>：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 使用 JetPack/L4T 官方 ROS 镜像（自带 GPU 与 CUDA 加速）</span>
docker run <span class="nt">-it</span> <span class="nt">--rm</span> <span class="se">\</span>
  <span class="nt">--runtime</span> nvidia <span class="se">\</span>
  <span class="nt">--network</span> host <span class="se">\</span>
  <span class="nt">-e</span> <span class="nv">DISPLAY</span><span class="o">=</span><span class="nv">$DISPLAY</span> <span class="se">\</span>
  <span class="nt">-v</span> /tmp/.X11-unix:/tmp/.X11-unix <span class="se">\</span>
  <span class="nt">-v</span> /dev:/dev <span class="nt">--privileged</span> <span class="se">\</span>
  nvcr.io/nvidia/l4t-ros:r35.4.1-ros2-humble-ros-base-l4t-r35.4.1
</code></pre></div></div>

<blockquote>
  <p><strong>⚠️ ARM64 关键提示</strong>（重点）：</p>
  <ul>
    <li>官方 <code class="language-plaintext highlighter-rouge">osrf/ros:*</code> 镜像<strong>基本只有 amd64</strong>。在 Jetson / 树莓派等 ARM64 机器上强行 <code class="language-plaintext highlighter-rouge">docker pull</code>，会提示 <code class="language-plaintext highlighter-rouge">platform does not match</code>，运行时走 qemu 模拟，<strong>几乎不可用</strong>。</li>
    <li>请改用 <strong>NVIDIA L4T 镜像</strong>，或自行基于 <code class="language-plaintext highlighter-rouge">ubuntu:22.04</code>（arm64）构建 —— <strong>ROS 2 官方 apt 源对 arm64 有原生二进制包</strong>，在容器内 <code class="language-plaintext highlighter-rouge">apt install ros-humble-desktop</code> 是可行且流畅的。</li>
  </ul>
</blockquote>

<h4 id="555-常见问题排查表">5.5.5 常见问题排查表</h4>

<table>
  <thead>
    <tr>
      <th>现象</th>
      <th>原因</th>
      <th>解决办法</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">No protocol specified</code></td>
      <td>Xauthority 未正确挂载</td>
      <td>加 <code class="language-plaintext highlighter-rouge">-v ~/.Xauthority:/root/.Xauthority:rw</code>，并执行 <code class="language-plaintext highlighter-rouge">xhost +local:docker</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">could not connect to display :0</code></td>
      <td><code class="language-plaintext highlighter-rouge">DISPLAY</code> 未传或 X 未运行</td>
      <td>检查 <code class="language-plaintext highlighter-rouge">-e DISPLAY</code>；宿主机先执行 <code class="language-plaintext highlighter-rouge">echo $DISPLAY</code> 确认值</td>
    </tr>
    <tr>
      <td>黑屏 / 闪退</td>
      <td>无 GPU 驱动，3D 渲染失败</td>
      <td>加 <code class="language-plaintext highlighter-rouge">LIBGL_ALWAYS_SOFTWARE=1</code> 软件渲染，或按方案 D 透传 GPU</td>
    </tr>
    <tr>
      <td>窗口能开但非常卡</td>
      <td>软件渲染吃 CPU</td>
      <td>使用方案 D 硬件加速；降低 Gazebo 分辨率/画质</td>
    </tr>
    <tr>
      <td>Qt 报共享内存错误</td>
      <td>容器与宿主机 X 的 IPC 限制</td>
      <td>加 <code class="language-plaintext highlighter-rouge">-e QT_X11_NO_MITSHM=1</code></td>
    </tr>
    <tr>
      <td>Gazebo 启动即崩溃</td>
      <td>3D 上下文初始化失败</td>
      <td><code class="language-plaintext highlighter-rouge">LIBGL_ALWAYS_SOFTWARE=1</code>，或 <code class="language-plaintext highlighter-rouge">gazebo --verbose</code> 查看具体报错</td>
    </tr>
  </tbody>
</table>

<p><strong>一句话总结</strong>：本地有屏 → <strong>方案 A（X11）</strong>；远程/无屏 → <strong>方案 B（VNC）</strong>；要流畅 → <strong>方案 D（GPU）</strong>；ARM 板子 → <strong>必须用 L4T 或自建 arm64 镜像</strong>。</p>

<hr />

<h2 id="六方案三python-工具链隔离">六、方案三：Python 工具链隔离</h2>

<p>如果你<strong>坚持在宿主机源码编译多个 ROS 2 发行版</strong>（不推荐），至少要把构建工具隔离，
因为 <code class="language-plaintext highlighter-rouge">colcon</code>、<code class="language-plaintext highlighter-rouge">rosdep</code>、<code class="language-plaintext highlighter-rouge">vcs</code> 等 Python 工具安装路径是共享的。</p>

<h3 id="61-用-pipx-或-venv-隔离工具链">6.1 用 pipx 或 venv 隔离工具链</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 方式 A：pipx（每个工具独立环境）</span>
pipx <span class="nb">install </span>colcon-common-extensions
pipx <span class="nb">install </span>rosdep
pipx <span class="nb">install </span>vcstool

<span class="c"># 方式 B：虚拟环境（更可控）</span>
python3 <span class="nt">-m</span> venv ~/ros_tools <span class="o">&amp;&amp;</span> <span class="nb">source</span> ~/ros_tools/bin/activate
pip <span class="nb">install </span>colcon-common-extensions rosdep vcstool
</code></pre></div></div>

<h3 id="62-源码编译不同发行版">6.2 源码编译不同发行版</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 以 Humble 为例（需在 Ubuntu 22.04）</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> ~/humble_src <span class="o">&amp;&amp;</span> <span class="nb">cd</span> ~/humble_src
wget https://raw.githubusercontent.com/ros2/ros2/humble/ros2.repos
vcs import src &lt; ros2.repos
rosdep init <span class="o">&amp;&amp;</span> rosdep update
rosdep <span class="nb">install</span> <span class="nt">--from-paths</span> src <span class="nt">--ignore-src</span> <span class="nt">-r</span> <span class="nt">-y</span>
colcon build <span class="nt">--symlink-install</span>
<span class="nb">source install</span>/setup.bash
</code></pre></div></div>

<blockquote>
  <p><strong>注意</strong>：源码编译不同发行版需要不同 Ubuntu/Python 环境，实际中极易因系统依赖冲突而失败，
多数情况下<strong>不值得</strong>。请优先考虑 Docker。</p>
</blockquote>

<hr />

<h2 id="七ros-1-与-ros-2-互通ros1_bridge">七、ROS 1 与 ROS 2 互通：ros1_bridge</h2>

<p>当你需要让 ROS 1 节点与 ROS 2 节点在同一台机器上互相通信时，使用 <code class="language-plaintext highlighter-rouge">ros1_bridge</code>。</p>

<h3 id="71-准备工作">7.1 准备工作</h3>

<ul>
  <li>ROS 1（如 Noetic）与 ROS 2（如 Jazzy）分别装在<strong>同一台宿主机或两个能互通的主机</strong>上。</li>
  <li>安装 bridge：</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install </span>ros-jazzy-ros1-bridge
</code></pre></div></div>

<h3 id="72-启动桥接">7.2 启动桥接</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 终端 1：启动 ROS 1</span>
<span class="nb">source</span> /opt/ros/noetic/setup.bash
roscore

<span class="c"># 终端 2：启动 ROS 2 并运行 bridge（先 source ROS 1 再 source ROS 2）</span>
<span class="nb">source</span> /opt/ros/noetic/setup.bash
<span class="nb">source</span> /opt/ros/jazzy/setup.bash
ros2 run ros1_bridge dynamic_bridge

<span class="c"># 终端 3：验证（ROS 1 侧发布）</span>
<span class="nb">source</span> /opt/ros/noetic/setup.bash
rostopic pub /chatter std_msgs/String <span class="s2">"data: 'hello'"</span> <span class="nt">-r</span> 1
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 终端 4：在 ROS 2 侧订阅</span>
<span class="nb">source</span> /opt/ros/jazzy/setup.bash
ros2 topic <span class="nb">echo</span> /chatter
</code></pre></div></div>

<blockquote>
  <p>前提：bridge 所在终端<strong>先 source ROS 1 再 source ROS 2</strong>，这样 bridge 能同时找到两套库。</p>
</blockquote>

<hr />

<h2 id="八日常检查与排查命令">八、日常检查与排查命令</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 查看当前 ROS 版本</span>
<span class="nb">echo</span> <span class="nv">$ROS_DISTRO</span>

<span class="c"># 查看工作空间 / 环境前缀</span>
<span class="nb">printenv</span> | <span class="nb">grep</span> <span class="nt">-E</span> <span class="s1">'ROS|AMENT|CMAKE_PREFIX_PATH|COLCON'</span>

<span class="c"># 确认当前是 ROS 1 还是 ROS 2</span>
<span class="nb">echo</span> <span class="nv">$ROS_VERSION</span>        <span class="c"># 1 或 2</span>

<span class="c"># 环境被污染时的急救：开一个干净终端</span>
<span class="nb">env</span> <span class="nt">-i</span> bash <span class="nt">--noprofile</span> <span class="nt">--norc</span>
<span class="nb">source</span> /opt/ros/jazzy/setup.bash

<span class="c"># Docker 中查看镜像 / 运行中的容器</span>
docker images
docker ps <span class="nt">-a</span>
</code></pre></div></div>

<hr />

<h2 id="九方案对比总结">九、方案对比总结</h2>

<table>
  <thead>
    <tr>
      <th>方案</th>
      <th>适用场景</th>
      <th>优点</th>
      <th>缺点</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>环境切换（source）</strong></td>
      <td>宿主机 ROS 1 + 同 Ubuntu 的 ROS 2</td>
      <td>零额外依赖、轻量</td>
      <td>只能支持宿主机对应发行版；工具链可能冲突</td>
    </tr>
    <tr>
      <td><strong>Docker 容器化</strong> ⭐</td>
      <td>任意版本组合（Humble/Jazzy/Noetic…）</td>
      <td>完全隔离、可复现、易分发、支持 GUI/硬件</td>
      <td>需学习 Docker；磁盘占用较大</td>
    </tr>
    <tr>
      <td><strong>Python 工具链隔离</strong></td>
      <td>源码编译多发行版的高级用户</td>
      <td>保留宿主机原生编译体验</td>
      <td>系统依赖冲突多、易踩坑</td>
    </tr>
  </tbody>
</table>

<h3 id="推荐实践路线">推荐实践路线</h3>

<ol>
  <li><strong>日常开发</strong>：优先 Docker，为每个项目写一个 <code class="language-plaintext highlighter-rouge">docker-compose.yml</code>，不同项目一键切换容器。</li>
  <li><strong>宿主机 ROS 1 + ROS 2 共存</strong>：用 <code class="language-plaintext highlighter-rouge">.bashrc</code> 切换函数（<code class="language-plaintext highlighter-rouge">use_ros1</code> / <code class="language-plaintext highlighter-rouge">use_ros2</code>），开新终端再切换。</li>
  <li><strong>跨版本通信</strong>：用 <code class="language-plaintext highlighter-rouge">ros1_bridge</code>（跨 ROS 1 / ROS 2）、不同 <code class="language-plaintext highlighter-rouge">ROS_DOMAIN_ID</code> 隔离 ROS 2 不同容器。</li>
  <li><strong>CI / 团队协作</strong>：把 Dockerfile 提交到仓库，任何人 <code class="language-plaintext highlighter-rouge">docker build</code> 都能得到一致环境。</li>
</ol>

<hr />

<p><em>参考资料：ROS 官方文档 (docs.ros.org)、OSRF Docker Hub (hub.docker.com/r/osrf/ros)、ros1_bridge 官方教程。</em></p>]]></content><author><name>老张同志</name></author><category term="ros2" /><category term="tutorial" /><summary type="html"><![CDATA[从环境变量冲突的本质讲起，对比「环境切换 / Docker 容器化 / Python 工具链隔离」三种多版本共存方案，附 ros1_bridge 互通与日常排查命令。]]></summary></entry><entry><title type="html">ROS 2 的 launch 文件</title><link href="https://1q08.github.io/ros_ws/ros2/tutorial/2026/08/05/ros2-launch.html" rel="alternate" type="text/html" title="ROS 2 的 launch 文件" /><published>2026-08-05T01:00:00+00:00</published><updated>2026-08-05T01:00:00+00:00</updated><id>https://1q08.github.io/ros_ws/ros2/tutorial/2026/08/05/ros2-launch</id><content type="html" xml:base="https://1q08.github.io/ros_ws/ros2/tutorial/2026/08/05/ros2-launch.html"><![CDATA[<h1 id="ros-2-的-launch-文件">ROS 2 的 launch 文件</h1>

<p>ROS 2 的 Launch 文件支持 <strong>Python（<code class="language-plaintext highlighter-rouge">.launch.py</code>）</strong>、<strong>XML（<code class="language-plaintext highlighter-rouge">.launch.xml</code>）</strong> 和 <strong>YAML（<code class="language-plaintext highlighter-rouge">.launch.yaml</code>）</strong> 三种格式。其中，Python 格式是官方和社区的首选，因为它提供了无与伦比的灵活性。</p>

<ul>
  <li>为什么首选 Python？
    <ul>
      <li><strong>灵活性高</strong>：可以使用完整的 Python 逻辑，如条件判断、循环、函数调用，实现动态和复杂的启动逻辑。</li>
      <li><strong>社区标准</strong>：已成为 ROS 2 社区的事实标准，资源和示例最丰富。</li>
      <li><strong>功能强大</strong>：能适配从简单到复杂的所有项目场景。</li>
    </ul>
  </li>
</ul>

<blockquote>
  <p>注意：XML 和 YAML 格式主要用于极简单的场景，在复杂项目中不推荐使用。</p>
</blockquote>

<hr />

<h2 id="一launch-文件的核心概念">一、launch 文件的核心概念</h2>

<ul>
  <li><strong>launch 文件是纯 Python 脚本</strong>：无需编译，<code class="language-plaintext highlighter-rouge">ros2 launch</code> 直接解释执行。</li>
  <li><strong><code class="language-plaintext highlighter-rouge">generate_launch_description()</code></strong>：每个 <code class="language-plaintext highlighter-rouge">.launch.py</code> 都必须定义这个函数，它是 launch 文件的<strong>入口点</strong>，返回值是一个 <code class="language-plaintext highlighter-rouge">LaunchDescription</code> 对象。</li>
  <li><strong><code class="language-plaintext highlighter-rouge">LaunchDescription</code></strong>：一个<strong>动作（Actions）列表</strong>，定义了要启动的节点、要设置的环境变量、要执行的进程等。</li>
  <li><strong>Action（动作）</strong>：<code class="language-plaintext highlighter-rouge">LaunchDescription</code> 里的每个元素。最常用的是 <code class="language-plaintext highlighter-rouge">Node</code>（启动一个节点），此外还有 <code class="language-plaintext highlighter-rouge">DeclareLaunchArgument</code>（声明启动参数）、<code class="language-plaintext highlighter-rouge">ExecuteProcess</code>（执行任意命令）等。</li>
</ul>

<p>基本骨架如下：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">launch</span> <span class="kn">import</span> <span class="n">LaunchDescription</span>

<span class="k">def</span> <span class="nf">generate_launch_description</span><span class="p">():</span>
    <span class="c1"># 在这里定义你的动作 (Actions)
</span>    <span class="k">return</span> <span class="n">LaunchDescription</span><span class="p">([</span>
        <span class="c1"># 动作列表，例如启动一个节点
</span>    <span class="p">])</span>
</code></pre></div></div>

<hr />

<h2 id="二基础启动--启动单个节点">二、基础启动 — 启动单个节点</h2>

<h3 id="21-写法-a完整限定名">2.1 写法 A：完整限定名</h3>

<p>使用 <code class="language-plaintext highlighter-rouge">import launch_ros.actions</code>，调用时写全名 <code class="language-plaintext highlighter-rouge">launch_ros.actions.Node</code>。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># import 方式一：import launch_ros.actions，使用时写全名 launch_ros.actions.Node
</span><span class="kn">from</span> <span class="nn">launch</span> <span class="kn">import</span> <span class="n">LaunchDescription</span>
<span class="kn">import</span> <span class="nn">launch_ros.actions</span>

<span class="k">def</span> <span class="nf">generate_launch_description</span><span class="p">():</span>
    <span class="c1"># 在这里定义你的动作 (Actions)
</span>    <span class="k">return</span> <span class="n">LaunchDescription</span><span class="p">([</span>
        <span class="c1"># 动作列表，例如启动一个节点
</span>        <span class="c1"># --- 最小示例：只指定 package + executable 两个必填字段 ---
</span>        <span class="n">launch_ros</span><span class="p">.</span><span class="n">actions</span><span class="p">.</span><span class="n">Node</span><span class="p">(</span>
            <span class="c1"># ---- 必须的字段 ----
</span>            <span class="n">package</span><span class="o">=</span><span class="s">'turtlesim'</span><span class="p">,</span>          <span class="c1"># 节点所在的功能包名
</span>            <span class="n">executable</span><span class="o">=</span><span class="s">'turtlesim_node'</span><span class="p">,</span>  <span class="c1"># 可执行文件名（这里直接用 turtlesim 包自带的节点程序）
</span>        <span class="p">),</span>
    <span class="p">])</span>
</code></pre></div></div>

<h3 id="22-写法-b直接导入-node-类推荐">2.2 写法 B：直接导入 Node 类（推荐）</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># import 方式二（推荐）：from ... import Node，使用时直接写 Node
</span><span class="kn">from</span> <span class="nn">launch</span> <span class="kn">import</span> <span class="n">LaunchDescription</span>
<span class="kn">from</span> <span class="nn">launch_ros.actions</span> <span class="kn">import</span> <span class="n">Node</span>

<span class="k">def</span> <span class="nf">generate_launch_description</span><span class="p">():</span>
    <span class="c1"># 在这里定义你的动作 (Actions)
</span>    <span class="k">return</span> <span class="n">LaunchDescription</span><span class="p">([</span>
        <span class="c1"># 动作列表，例如启动一个节点
</span>        <span class="c1"># --- 最小示例：只指定 package + executable ---
</span>        <span class="n">Node</span><span class="p">(</span>
            <span class="c1"># ---- 必须的字段 ----
</span>            <span class="n">package</span><span class="o">=</span><span class="s">'turtlesim'</span><span class="p">,</span>          <span class="c1"># 节点所在的功能包名
</span>            <span class="n">executable</span><span class="o">=</span><span class="s">'turtlesim_node'</span><span class="p">,</span>  <span class="c1"># 可执行文件名（这里直接用 turtlesim 包自带的节点程序）
</span>        <span class="p">)</span>
    <span class="p">])</span>
</code></pre></div></div>

<blockquote>
  <p>与写法 A 效果完全一样，只是代码更简洁、更符合规范。两种写法二选一即可，本文其余章节均采用写法 B。</p>
</blockquote>

<blockquote>
  <p><strong>保存与运行</strong>：把上面的内容（写法 A 或 B 任选其一）保存为 <code class="language-plaintext highlighter-rouge">~/ros2_launch_demo/01_basic.launch.py</code>，然后在终端运行：</p>

  <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ros2 launch ~/ros2_launch_demo/01_basic.launch.py
</code></pre></div>  </div>

  <p>运行后会弹出一个 <strong>turtlesim 窗口</strong>。本文的示例文件统一放在 <code class="language-plaintext highlighter-rouge">~/ros2_launch_demo/</code> 目录下，都可以直接复制粘贴保存后运行。</p>
</blockquote>

<hr />

<h2 id="三完整节点配置--展示所有常用字段">三、完整节点配置 — 展示所有常用字段</h2>

<p>本节把所有字段堆在一起展示，实际使用时可按需删减。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">launch</span> <span class="kn">import</span> <span class="n">LaunchDescription</span>
<span class="kn">from</span> <span class="nn">launch_ros.actions</span> <span class="kn">import</span> <span class="n">Node</span>

<span class="k">def</span> <span class="nf">generate_launch_description</span><span class="p">():</span>
    <span class="c1"># 在这里定义你的动作 (Actions)
</span>    <span class="k">return</span> <span class="n">LaunchDescription</span><span class="p">([</span>
        <span class="c1"># 动作列表，例如启动一个节点
</span>        <span class="n">Node</span><span class="p">(</span>
            <span class="c1"># ---- 必须的字段 ----
</span>            <span class="n">package</span><span class="o">=</span><span class="s">'turtlesim'</span><span class="p">,</span>          <span class="c1"># 节点所在的功能包名
</span>            <span class="n">executable</span><span class="o">=</span><span class="s">'turtlesim_node'</span><span class="p">,</span>  <span class="c1"># 可执行文件名（这里直接用 turtlesim 包自带的节点程序）
</span>
            <span class="c1"># ---- 可选字段 ----
</span>            <span class="n">name</span><span class="o">=</span><span class="s">'my_turtle'</span><span class="p">,</span>             <span class="c1"># 给节点改名（不指定则默认用 executable 的名字）
</span>            <span class="n">namespace</span><span class="o">=</span><span class="s">'ns1'</span><span class="p">,</span>              <span class="c1"># 放入命名空间 → 节点全名变成 /ns1/my_turtle
</span>                                          <span class="c1"># 话题也随之变成 /ns1/...，用于多实例隔离
</span>            <span class="n">output</span><span class="o">=</span><span class="s">'screen'</span><span class="p">,</span>              <span class="c1"># 将 stdout/stderr 打印到终端（也可选 'log' 或 'both'）
</span>
            <span class="c1"># ---- 设置 ROS 参数 ----
</span>            <span class="c1">#   参数会在节点启动时加载，可用 ros2 param list 查看
</span>            <span class="c1">#   数字会按 YAML 规则处理：字符串数字（如 '255'）会自动转成 int
</span>            <span class="n">parameters</span><span class="o">=</span><span class="p">[{</span>
                <span class="s">'background_r'</span><span class="p">:</span> <span class="mi">255</span><span class="p">,</span>      <span class="c1"># 背景色 R 通道 (0-255)
</span>                <span class="s">'background_g'</span><span class="p">:</span> <span class="mi">165</span><span class="p">,</span>      <span class="c1"># 背景色 G 通道 (0-255)
</span>                <span class="s">'background_b'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>        <span class="c1"># 背景色 B 通道 (0-255)
</span>            <span class="p">}],</span>

            <span class="c1"># ---- 话题重映射 (Remapping) ----
</span>            <span class="c1">#   每个元素是 (旧话题, 新话题)，相当于把 /cmd_vel "改名为" /turtle1/cmd_vel
</span>            <span class="n">remappings</span><span class="o">=</span><span class="p">[</span>
                <span class="p">(</span><span class="s">'/cmd_vel'</span><span class="p">,</span> <span class="s">'/turtle1/cmd_vel'</span><span class="p">),</span>
            <span class="p">],</span>

            <span class="c1"># ---- 额外 ROS 参数（透传给 --ros-args） ----
</span>            <span class="c1">#   arguments 里的内容会"原样"拼接到节点命令后面（等价于 ros2 run 之后
</span>            <span class="c1">#   再加的参数），必须以 --ros-args 开头，用来控制 ROS 2 运行时的行为。
</span>            <span class="c1">#   常见用法：
</span>            <span class="c1">#     --log-level INFO          设置日志级别 (DEBUG/INFO/WARN/ERROR/FATAL)
</span>            <span class="c1">#     --params-file config.yaml 从 YAML 文件加载参数（等价于 parameters 字段）
</span>            <span class="c1">#     -p key:=value             直接设置单条参数
</span>            <span class="c1">#     -r 旧话题:=新话题         话题重映射（等价于 remappings 字段）
</span>            <span class="c1">#   注意：parameters / remappings 字段本质上是这些 --ros-args 的
</span>            <span class="c1">#   "结构化便捷写法"，最终都会被转换成 --ros-args 透传给节点。
</span>            <span class="n">arguments</span><span class="o">=</span><span class="p">[</span><span class="s">'--ros-args'</span><span class="p">,</span> <span class="s">'--log-level'</span><span class="p">,</span> <span class="s">'INFO'</span><span class="p">],</span>

        <span class="p">),</span>
    <span class="p">])</span>
</code></pre></div></div>

<blockquote>
  <p><strong>保存与运行</strong>：保存为 <code class="language-plaintext highlighter-rouge">~/ros2_launch_demo/02_full.launch.py</code>，运行：</p>

  <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ros2 launch ~/ros2_launch_demo/02_full.launch.py
</code></pre></div>  </div>

  <p>这次节点名变成了 <code class="language-plaintext highlighter-rouge">/ns1/my_turtle</code>（命名空间 + 改名生效），背景色是橙色。</p>
</blockquote>

<hr />

<h2 id="四多节点启动--命名空间隔离">四、多节点启动 + 命名空间隔离</h2>

<blockquote>
  <p>规则：</p>

  <ul>
    <li>同一 <code class="language-plaintext highlighter-rouge">package</code> + 同一 <code class="language-plaintext highlighter-rouge">executable</code> + 同一 <code class="language-plaintext highlighter-rouge">name</code> → 需要不同 <code class="language-plaintext highlighter-rouge">namespace</code></li>
    <li>同一 <code class="language-plaintext highlighter-rouge">package</code> + 同一 <code class="language-plaintext highlighter-rouge">executable</code> + 同一 <code class="language-plaintext highlighter-rouge">namespace</code> → 需要不同 <code class="language-plaintext highlighter-rouge">name</code></li>
  </ul>
</blockquote>

<h3 id="41-写法-a节点直接内联在-launchdescription-列表中">4.1 写法 A：节点直接内联在 LaunchDescription 列表中</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">launch</span> <span class="kn">import</span> <span class="n">LaunchDescription</span>
<span class="kn">import</span> <span class="nn">launch_ros.actions</span>  <span class="c1"># 写法 A：import 包，使用时写全名 launch_ros.actions.Node
</span>
<span class="k">def</span> <span class="nf">generate_launch_description</span><span class="p">():</span>
    <span class="k">return</span> <span class="n">LaunchDescription</span><span class="p">([</span>
        <span class="c1"># ---- 第一个节点：放入 turtlesim1 命名空间 ----
</span>        <span class="n">launch_ros</span><span class="p">.</span><span class="n">actions</span><span class="p">.</span><span class="n">Node</span><span class="p">(</span>
            <span class="n">package</span><span class="o">=</span><span class="s">'turtlesim'</span><span class="p">,</span>          <span class="c1"># 节点所在的功能包名
</span>            <span class="n">executable</span><span class="o">=</span><span class="s">'turtlesim_node'</span><span class="p">,</span>  <span class="c1"># 可执行文件名（这里直接用 turtlesim 包自带的节点程序）
</span>            <span class="n">namespace</span><span class="o">=</span><span class="s">'turtlesim1'</span><span class="p">,</span>       <span class="c1"># 节点全名变成 /turtlesim1/turtle1
</span>                                          <span class="c1"># 通过 namespace 隔离，两个同类型节点可以共存
</span>            <span class="n">name</span><span class="o">=</span><span class="s">'turtle1'</span><span class="p">,</span>               <span class="c1"># 节点名
</span>            <span class="n">parameters</span><span class="o">=</span><span class="p">[{</span><span class="s">'background_r'</span><span class="p">:</span> <span class="mi">255</span><span class="p">,</span> <span class="s">'background_g'</span><span class="p">:</span> <span class="mi">165</span><span class="p">,</span> <span class="s">'background_b'</span><span class="p">:</span> <span class="mi">0</span><span class="p">}],</span>  <span class="c1"># 设置背景色参数
</span>        <span class="p">),</span>
        <span class="c1"># ---- 第二个节点：放入 turtlesim2 命名空间 ----
</span>        <span class="n">launch_ros</span><span class="p">.</span><span class="n">actions</span><span class="p">.</span><span class="n">Node</span><span class="p">(</span>
            <span class="n">package</span><span class="o">=</span><span class="s">'turtlesim'</span><span class="p">,</span>          <span class="c1"># 节点所在的功能包名
</span>            <span class="n">executable</span><span class="o">=</span><span class="s">'turtlesim_node'</span><span class="p">,</span>  <span class="c1"># 可执行文件名
</span>            <span class="n">namespace</span><span class="o">=</span><span class="s">'turtlesim2'</span><span class="p">,</span>       <span class="c1"># 与第一个节点同名，靠不同 namespace 区分
</span>            <span class="n">name</span><span class="o">=</span><span class="s">'turtle1'</span><span class="p">,</span>               <span class="c1"># 节点名（与第一个节点相同）
</span>            <span class="n">parameters</span><span class="o">=</span><span class="p">[{</span><span class="s">'background_r'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s">'background_g'</span><span class="p">:</span> <span class="mi">255</span><span class="p">,</span> <span class="s">'background_b'</span><span class="p">:</span> <span class="mi">100</span><span class="p">}],</span>  <span class="c1"># 设置背景色参数
</span>        <span class="p">),</span>
    <span class="p">])</span>
</code></pre></div></div>

<h3 id="42-写法-b先把每个节点赋给变量再放入列表便于复用修改">4.2 写法 B：先把每个节点赋给变量，再放入列表（便于复用/修改）</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">launch</span> <span class="kn">import</span> <span class="n">LaunchDescription</span>
<span class="kn">from</span> <span class="nn">launch_ros.actions</span> <span class="kn">import</span> <span class="n">Node</span>  <span class="c1"># 写法 B：直接导入 Node 类，使用时写 Node
</span>
<span class="k">def</span> <span class="nf">generate_launch_description</span><span class="p">():</span>
    <span class="c1"># 先把每个节点赋给变量，再统一放入列表（便于复用/修改）
</span>    <span class="n">turtlesim1_node</span> <span class="o">=</span> <span class="n">Node</span><span class="p">(</span>           <span class="c1"># 第一个节点：放入 turtlesim1 命名空间
</span>        <span class="n">package</span><span class="o">=</span><span class="s">'turtlesim'</span><span class="p">,</span>          <span class="c1"># 节点所在的功能包名
</span>        <span class="n">executable</span><span class="o">=</span><span class="s">'turtlesim_node'</span><span class="p">,</span>  <span class="c1"># 可执行文件名（这里直接用 turtlesim 包自带的节点程序）
</span>        <span class="n">namespace</span><span class="o">=</span><span class="s">'turtlesim1'</span><span class="p">,</span>       <span class="c1"># 节点全名变成 /turtlesim1/turtle1
</span>                                      <span class="c1"># 通过 namespace 隔离，两个同类型节点可以共存
</span>        <span class="n">name</span><span class="o">=</span><span class="s">'turtle1'</span><span class="p">,</span>                <span class="c1"># 节点名
</span>        <span class="n">parameters</span><span class="o">=</span><span class="p">[{</span><span class="s">'background_r'</span><span class="p">:</span> <span class="mi">255</span><span class="p">,</span> <span class="s">'background_g'</span><span class="p">:</span> <span class="mi">165</span><span class="p">,</span> <span class="s">'background_b'</span><span class="p">:</span> <span class="mi">0</span><span class="p">}],</span>  <span class="c1"># 设置背景色参数
</span>    <span class="p">)</span>
    <span class="n">turtlesim2_node</span> <span class="o">=</span> <span class="n">Node</span><span class="p">(</span>           <span class="c1"># 第二个节点：放入 turtlesim2 命名空间
</span>        <span class="n">package</span><span class="o">=</span><span class="s">'turtlesim'</span><span class="p">,</span>          <span class="c1"># 节点所在的功能包名
</span>        <span class="n">executable</span><span class="o">=</span><span class="s">'turtlesim_node'</span><span class="p">,</span>  <span class="c1"># 可执行文件名
</span>        <span class="n">namespace</span><span class="o">=</span><span class="s">'turtlesim2'</span><span class="p">,</span>       <span class="c1"># 与第一个节点同名，靠不同 namespace 区分
</span>        <span class="n">name</span><span class="o">=</span><span class="s">'turtle1'</span><span class="p">,</span>               <span class="c1"># 节点名（与第一个节点相同）
</span>        <span class="n">parameters</span><span class="o">=</span><span class="p">[{</span><span class="s">'background_r'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s">'background_g'</span><span class="p">:</span> <span class="mi">255</span><span class="p">,</span> <span class="s">'background_b'</span><span class="p">:</span> <span class="mi">100</span><span class="p">}],</span>  <span class="c1"># 设置背景色参数
</span>    <span class="p">)</span>
    <span class="k">return</span> <span class="n">LaunchDescription</span><span class="p">([</span>
        <span class="n">turtlesim1_node</span><span class="p">,</span>              <span class="c1"># 组装到 LaunchDescription
</span>        <span class="n">turtlesim2_node</span>
    <span class="p">])</span>
</code></pre></div></div>

<blockquote>
  <p><strong>保存与运行</strong>：保存为 <code class="language-plaintext highlighter-rouge">~/ros2_launch_demo/03_multi.launch.py</code>（写法 A、B 任选其一），运行：</p>

  <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ros2 launch ~/ros2_launch_demo/03_multi.launch.py
</code></pre></div>  </div>

  <p>会同时弹出两个 turtlesim 窗口，分别对应 <code class="language-plaintext highlighter-rouge">/turtlesim1/turtle1</code> 和 <code class="language-plaintext highlighter-rouge">/turtlesim2/turtle1</code>。</p>
</blockquote>

<hr />

<h2 id="五节点参数配置--命令行传参与-yaml-参数文件">五、节点参数配置 — 命令行传参与 YAML 参数文件</h2>

<p>通过 <code class="language-plaintext highlighter-rouge">DeclareLaunchArgument</code> 声明可配置参数，再用 <code class="language-plaintext highlighter-rouge">LaunchConfiguration</code> 获取其值，即可在启动时动态传入参数：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ros2 launch ~/ros2_launch_demo/04_param_single.launch.py background_r:<span class="o">=</span>200
</code></pre></div></div>

<h3 id="51-单参数版">5.1 单参数版</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">launch</span> <span class="kn">import</span> <span class="n">LaunchDescription</span>
<span class="kn">from</span> <span class="nn">launch_ros.actions</span> <span class="kn">import</span> <span class="n">Node</span>
<span class="kn">from</span> <span class="nn">launch.actions</span> <span class="kn">import</span> <span class="n">DeclareLaunchArgument</span>      <span class="c1"># 定义可配置参数
</span><span class="kn">from</span> <span class="nn">launch.substitutions</span> <span class="kn">import</span> <span class="n">LaunchConfiguration</span>  <span class="c1"># 参数获取
</span>
<span class="k">def</span> <span class="nf">generate_launch_description</span><span class="p">():</span>
    <span class="c1"># 1. 声明一个名为 background_r 的启动参数
</span>    <span class="n">background_r_arg</span> <span class="o">=</span> <span class="n">DeclareLaunchArgument</span><span class="p">(</span>
        <span class="s">'background_r'</span><span class="p">,</span>          <span class="c1"># 参数名称（必需）
</span>        <span class="n">default_value</span><span class="o">=</span><span class="s">'150'</span><span class="p">,</span>     <span class="c1"># 默认值（可选，若不提供则用户必须传入，且必须是字符串）
</span>        <span class="n">description</span><span class="o">=</span><span class="s">'背景色 R 通道 (0-255)'</span>  <span class="c1"># 描述信息（可选，用于 --show-args）
</span>    <span class="p">)</span>

    <span class="c1"># 2. 获取参数的实际值（占位符）
</span>    <span class="n">background_r</span> <span class="o">=</span> <span class="n">LaunchConfiguration</span><span class="p">(</span><span class="s">'background_r'</span><span class="p">)</span>

    <span class="c1"># 3. 定义 turtlesim 节点，使用 background_r 参数
</span>    <span class="n">turtlesim_node</span> <span class="o">=</span> <span class="n">Node</span><span class="p">(</span>
        <span class="n">namespace</span><span class="o">=</span><span class="s">'ns1'</span><span class="p">,</span>             <span class="c1"># 放入指定的命名空间（可选）
</span>        <span class="n">package</span><span class="o">=</span><span class="s">'turtlesim'</span><span class="p">,</span>         <span class="c1"># 节点所在的功能包名
</span>        <span class="n">executable</span><span class="o">=</span><span class="s">'turtlesim_node'</span><span class="p">,</span> <span class="c1"># 要运行的可执行文件名
</span>        <span class="n">name</span><span class="o">=</span><span class="s">'my_turtle'</span><span class="p">,</span>            <span class="c1"># 给这个节点起一个新名字（可选）
</span>        <span class="n">output</span><span class="o">=</span><span class="s">'screen'</span><span class="p">,</span>             <span class="c1"># 将日志打印到屏幕（而不是日志文件）
</span>        <span class="n">parameters</span><span class="o">=</span><span class="p">[{</span><span class="s">'background_r'</span><span class="p">:</span> <span class="n">background_r</span><span class="p">}],</span>  <span class="c1"># 设置参数
</span>    <span class="p">)</span>

    <span class="c1"># 4. 组装到 LaunchDescription
</span>    <span class="k">return</span> <span class="n">LaunchDescription</span><span class="p">([</span>
        <span class="n">background_r_arg</span><span class="p">,</span>
        <span class="n">turtlesim_node</span>
    <span class="p">])</span>
</code></pre></div></div>

<blockquote>
  <p><strong>保存与运行</strong>：保存为 <code class="language-plaintext highlighter-rouge">~/ros2_launch_demo/04_param_single.launch.py</code>，运行（可自定义背景色）：</p>

  <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ros2 launch ~/ros2_launch_demo/04_param_single.launch.py background_r:<span class="o">=</span>200
</code></pre></div>  </div>
</blockquote>

<h3 id="52-多参数版写法更紧凑">5.2 多参数版（写法更紧凑）</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">launch</span> <span class="kn">import</span> <span class="n">LaunchDescription</span>
<span class="kn">from</span> <span class="nn">launch_ros.actions</span> <span class="kn">import</span> <span class="n">Node</span>
<span class="kn">from</span> <span class="nn">launch.actions</span> <span class="kn">import</span> <span class="n">DeclareLaunchArgument</span>      <span class="c1"># 定义可配置参数
</span><span class="kn">from</span> <span class="nn">launch.substitutions</span> <span class="kn">import</span> <span class="n">LaunchConfiguration</span>  <span class="c1"># 参数获取
</span>
<span class="k">def</span> <span class="nf">generate_launch_description</span><span class="p">():</span>
    <span class="s">"""支持命令行传参的写法，例如:
       ros2 launch ~/ros2_launch_demo/04_param.launch.py background_r:=200 background_g:=100 background_b:=50
    """</span>

    <span class="c1"># 1. 声明有哪些启动参数及其默认值
</span>    <span class="n">declare_bg_r</span> <span class="o">=</span> <span class="n">DeclareLaunchArgument</span><span class="p">(</span><span class="s">'background_r'</span><span class="p">,</span> <span class="n">default_value</span><span class="o">=</span><span class="s">'150'</span><span class="p">)</span>
    <span class="n">declare_bg_g</span> <span class="o">=</span> <span class="n">DeclareLaunchArgument</span><span class="p">(</span><span class="s">'background_g'</span><span class="p">,</span> <span class="n">default_value</span><span class="o">=</span><span class="s">'86'</span><span class="p">)</span>
    <span class="n">declare_bg_b</span> <span class="o">=</span> <span class="n">DeclareLaunchArgument</span><span class="p">(</span><span class="s">'background_b'</span><span class="p">,</span> <span class="n">default_value</span><span class="o">=</span><span class="s">'255'</span><span class="p">)</span>

    <span class="c1"># 2. 用 LaunchConfiguration 获取占位符（此时值还未确定，运行时才替换）
</span>    <span class="n">bg_r</span> <span class="o">=</span> <span class="n">LaunchConfiguration</span><span class="p">(</span><span class="s">'background_r'</span><span class="p">)</span>
    <span class="n">bg_g</span> <span class="o">=</span> <span class="n">LaunchConfiguration</span><span class="p">(</span><span class="s">'background_g'</span><span class="p">)</span>
    <span class="n">bg_b</span> <span class="o">=</span> <span class="n">LaunchConfiguration</span><span class="p">(</span><span class="s">'background_b'</span><span class="p">)</span>

    <span class="c1"># 3. 传给节点
</span>    <span class="n">node</span> <span class="o">=</span> <span class="n">Node</span><span class="p">(</span>
        <span class="n">package</span><span class="o">=</span><span class="s">'turtlesim'</span><span class="p">,</span>
        <span class="n">executable</span><span class="o">=</span><span class="s">'turtlesim_node'</span><span class="p">,</span>
        <span class="n">parameters</span><span class="o">=</span><span class="p">[{</span>
            <span class="s">'background_r'</span><span class="p">:</span> <span class="n">bg_r</span><span class="p">,</span>
            <span class="s">'background_g'</span><span class="p">:</span> <span class="n">bg_g</span><span class="p">,</span>
            <span class="s">'background_b'</span><span class="p">:</span> <span class="n">bg_b</span><span class="p">,</span>
        <span class="p">}],</span>
    <span class="p">)</span>

    <span class="c1"># 4. 注意：LaunchDescription 中必须先列出 DeclareLaunchArgument，再列出 Node
</span>    <span class="k">return</span> <span class="n">LaunchDescription</span><span class="p">([</span>
        <span class="n">declare_bg_r</span><span class="p">,</span>
        <span class="n">declare_bg_g</span><span class="p">,</span>
        <span class="n">declare_bg_b</span><span class="p">,</span>
        <span class="n">node</span><span class="p">,</span>
    <span class="p">])</span>
</code></pre></div></div>

<blockquote>
  <p><strong>保存与运行</strong>：保存为 <code class="language-plaintext highlighter-rouge">~/ros2_launch_demo/04_param.launch.py</code>，运行（多参数版）：</p>

  <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ros2 launch ~/ros2_launch_demo/04_param.launch.py background_r:<span class="o">=</span>200 background_g:<span class="o">=</span>100 background_b:<span class="o">=</span>50
</code></pre></div>  </div>
</blockquote>

<h3 id="53-从-yaml-参数文件加载参数多时的最佳实践">5.3 从 YAML 参数文件加载（参数多时的最佳实践）</h3>

<p>前面两小节是通过<strong>命令行</strong>传参。但当参数很多（比如几十上百个）时，每次都写命令行很痛苦。更常见的做法是：把参数集中写在一个 <strong>YAML 参数文件</strong>里，让节点启动时直接加载。</p>

<p><strong>第一步：准备 YAML 参数文件</strong> <code class="language-plaintext highlighter-rouge">~/ros2_launch_demo/config/turtlesim.yaml</code>：</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/turtlesim.yaml</span>
<span class="c1"># ROS 2 参数文件的格式：最外层是"节点全名"，下面固定是 ros__parameters 键</span>
<span class="na">/turtlesim2/sim</span><span class="pi">:</span>                <span class="c1"># 节点全名 = 命名空间/节点名（必须与 launch 里节点一致）</span>
   <span class="na">ros__parameters</span><span class="pi">:</span>             <span class="c1"># 固定写法：所有参数都放在这个键下面</span>
      <span class="na">background_b</span><span class="pi">:</span> <span class="m">255</span>         <span class="c1"># 背景色 B 通道 (0-255)</span>
      <span class="na">background_g</span><span class="pi">:</span> <span class="m">86</span>          <span class="c1"># 背景色 G 通道 (0-255)</span>
      <span class="na">background_r</span><span class="pi">:</span> <span class="m">150</span>         <span class="c1"># 背景色 R 通道 (0-255)</span>
</code></pre></div></div>

<blockquote>
  <p>参数文件的顶层键是<strong>节点全名</strong>。上例对应一个 <code class="language-plaintext highlighter-rouge">namespace='turtlesim2'</code>、<code class="language-plaintext highlighter-rouge">name='sim'</code> 的节点（全名 <code class="language-plaintext highlighter-rouge">/turtlesim2/sim</code>）。</p>
</blockquote>

<p><strong>第二步：launch 文件</strong> <code class="language-plaintext highlighter-rouge">~/ros2_launch_demo/05_yaml.launch.py</code>：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 05_yaml.launch.py
# 作用：演示从 YAML 参数文件加载参数（parameters 里放文件路径，而不是字典）
</span><span class="kn">import</span> <span class="nn">os</span>  <span class="c1"># 用于拼接文件路径
</span>
<span class="kn">from</span> <span class="nn">launch</span> <span class="kn">import</span> <span class="n">LaunchDescription</span>
<span class="kn">from</span> <span class="nn">launch_ros.actions</span> <span class="kn">import</span> <span class="n">Node</span>

<span class="k">def</span> <span class="nf">generate_launch_description</span><span class="p">():</span>
    <span class="c1"># 1. 定位 YAML 参数文件的绝对路径
</span>    <span class="c1">#    os.path.dirname(__file__) 返回当前文件所在目录（本文件与 config/ 同目录）
</span>    <span class="n">config</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span>
        <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">dirname</span><span class="p">(</span><span class="n">__file__</span><span class="p">),</span>   <span class="c1"># 当前 launch 文件所在目录
</span>        <span class="s">'config'</span><span class="p">,</span>                    <span class="c1"># config 子目录
</span>        <span class="s">'turtlesim.yaml'</span>             <span class="c1"># 参数文件名
</span>    <span class="p">)</span>

    <span class="c1"># 2. 启动节点，parameters 直接放"文件路径"（字符串）
</span>    <span class="c1">#    节点名（namespace + name）必须与 YAML 顶层键 /turtlesim2/sim 一致
</span>    <span class="k">return</span> <span class="n">LaunchDescription</span><span class="p">([</span>
        <span class="n">Node</span><span class="p">(</span>
            <span class="n">package</span><span class="o">=</span><span class="s">'turtlesim'</span><span class="p">,</span>          <span class="c1"># 节点所在的功能包名
</span>            <span class="n">executable</span><span class="o">=</span><span class="s">'turtlesim_node'</span><span class="p">,</span>  <span class="c1"># 可执行文件名
</span>            <span class="n">namespace</span><span class="o">=</span><span class="s">'turtlesim2'</span><span class="p">,</span>       <span class="c1"># 命名空间（与 YAML 顶层键对应）
</span>            <span class="n">name</span><span class="o">=</span><span class="s">'sim'</span><span class="p">,</span>                   <span class="c1"># 节点名（与 YAML 顶层键对应）
</span>            <span class="n">parameters</span><span class="o">=</span><span class="p">[</span><span class="n">config</span><span class="p">]</span>           <span class="c1"># 从 YAML 文件加载参数（放路径而非字典）
</span>        <span class="p">),</span>
    <span class="p">])</span>
</code></pre></div></div>

<blockquote>
  <p><strong>保存与运行</strong>：先把上面的 YAML 保存为 <code class="language-plaintext highlighter-rouge">~/ros2_launch_demo/config/turtlesim.yaml</code>，再把 launch 保存为 <code class="language-plaintext highlighter-rouge">~/ros2_launch_demo/05_yaml.launch.py</code>，运行：</p>

  <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ros2 launch ~/ros2_launch_demo/05_yaml.launch.py
</code></pre></div>  </div>

  <p>运行后可用以下命令确认参数确实从 YAML 加载成功：</p>

  <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ros2 param get /turtlesim2/sim background_r   <span class="c"># 应输出 150（来自 YAML 文件）</span>
ros2 param list /turtlesim2/sim               <span class="c"># 列出该节点所有参数</span>
</code></pre></div>  </div>
</blockquote>

<p><strong>关键点说明</strong>：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">parameters</code> 里放<strong>字典</strong> <code class="language-plaintext highlighter-rouge">{...}</code> → 表示”直接设置参数”；放<strong>字符串路径</strong> → 表示”从 YAML 文件加载”。两者可以混用：
    <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">parameters</span><span class="o">=</span><span class="p">[</span><span class="n">config</span><span class="p">,</span> <span class="p">{</span><span class="s">'background_r'</span><span class="p">:</span> <span class="mi">0</span><span class="p">}]</span>  <span class="c1"># 先加载 YAML，再用字典覆盖其中一个
</span></code></pre></div>    </div>
  </li>
  <li>YAML 的顶层键（节点全名）必须和 launch 里节点的<strong>命名空间 + 节点名</strong>完全一致，否则参数不会生效（ROS 会报”没有找到该节点对应的参数”）。</li>
  <li>如果节点没设置 <code class="language-plaintext highlighter-rouge">namespace</code>/<code class="language-plaintext highlighter-rouge">name</code>，则全名就是 <code class="language-plaintext highlighter-rouge">/executable名</code>，YAML 顶层键要写成 <code class="language-plaintext highlighter-rouge">/turtlesim_node:</code>。</li>
</ul>

<blockquote>
  <p><strong>常见坑</strong>：</p>

  <ul>
    <li><strong>节点名对不上</strong>：YAML 顶层键写的是 <code class="language-plaintext highlighter-rouge">/turtlesim2/sim</code>，但 launch 里节点没写 <code class="language-plaintext highlighter-rouge">namespace='turtlesim2'</code> 和 <code class="language-plaintext highlighter-rouge">name='sim'</code> → 参数静默不加载。用 <code class="language-plaintext highlighter-rouge">ros2 param list</code> 检查是否为空即可发现。</li>
    <li><strong>功能包里的 YAML 忘了安装</strong>：如果把 YAML 放进功能包并用 <code class="language-plaintext highlighter-rouge">get_package_share_directory()</code> 定位（生产环境推荐），记得像第九节那样在 <code class="language-plaintext highlighter-rouge">setup.py</code> / <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> 里<strong>安装 config 目录</strong>，否则启动时报”参数文件不存在”。本功能包的 <code class="language-plaintext highlighter-rouge">setup.py</code> 已有 <code class="language-plaintext highlighter-rouge">glob('config/*.yaml')</code> 安装规则。</li>
    <li><strong>生产环境定位方式</strong>：示例用 <code class="language-plaintext highlighter-rouge">os.path.dirname(__file__)</code> 方便快速测试；功能包里应改用：
      <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">ament_index_python.packages</span> <span class="kn">import</span> <span class="n">get_package_share_directory</span>
<span class="n">config</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span>
    <span class="n">get_package_share_directory</span><span class="p">(</span><span class="s">'launch_tutorial'</span><span class="p">),</span> <span class="s">'config'</span><span class="p">,</span> <span class="s">'turtlesim.yaml'</span><span class="p">)</span>
</code></pre></div>      </div>
    </li>
  </ul>
</blockquote>

<blockquote>
  <p><strong>三种设置节点参数的方式对比</strong>：</p>

  <table>
    <thead>
      <tr>
        <th>方式</th>
        <th>写法</th>
        <th>适用场景</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>命令行传参（5.1 / 5.2 节）</td>
        <td><code class="language-plaintext highlighter-rouge">DeclareLaunchArgument</code> + <code class="language-plaintext highlighter-rouge">parameters=[{'key': LaunchConfiguration('key')}]</code></td>
        <td>参数少、需要每次启动临时改</td>
      </tr>
      <tr>
        <td>直接写死</td>
        <td><code class="language-plaintext highlighter-rouge">parameters=[{'key': value}]</code></td>
        <td>参数少且固定不变</td>
      </tr>
      <tr>
        <td>YAML 参数文件（本节）</td>
        <td><code class="language-plaintext highlighter-rouge">parameters=[文件路径]</code></td>
        <td>参数多、需集中管理</td>
      </tr>
    </tbody>
  </table>
</blockquote>

<hr />

<h2 id="六条件启动-conditional-launch">六、条件启动 (Conditional Launch)</h2>

<p>使用 <code class="language-plaintext highlighter-rouge">IfCondition</code> 可以根据命令行参数值决定是否启动某个节点。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">launch</span> <span class="kn">import</span> <span class="n">LaunchDescription</span>
<span class="kn">from</span> <span class="nn">launch_ros.actions</span> <span class="kn">import</span> <span class="n">Node</span>
<span class="kn">from</span> <span class="nn">launch.conditions</span> <span class="kn">import</span> <span class="n">IfCondition</span>  <span class="c1"># 条件启动
</span><span class="kn">from</span> <span class="nn">launch.substitutions</span> <span class="kn">import</span> <span class="n">LaunchConfiguration</span>  <span class="c1"># 获取命令行参数值
</span>
<span class="k">def</span> <span class="nf">generate_launch_description</span><span class="p">():</span>
    <span class="c1"># 读取命令行参数 launch_turtlesim（默认 true），如：ros2 launch 05_condition.launch.py launch_turtlesim1:=false
</span>    <span class="n">launch_turtlesim1</span> <span class="o">=</span> <span class="n">LaunchConfiguration</span><span class="p">(</span><span class="s">'launch_turtlesim1'</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="s">'true'</span><span class="p">)</span>
    <span class="n">launch_turtlesim2</span> <span class="o">=</span> <span class="n">LaunchConfiguration</span><span class="p">(</span><span class="s">'launch_turtlesim2'</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="s">'true'</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">LaunchDescription</span><span class="p">([</span>
        <span class="c1"># ---- 第一个节点 ----
</span>        <span class="n">Node</span><span class="p">(</span>
            <span class="n">package</span><span class="o">=</span><span class="s">'turtlesim'</span><span class="p">,</span>
            <span class="n">executable</span><span class="o">=</span><span class="s">'turtlesim_node'</span><span class="p">,</span>
            <span class="n">name</span><span class="o">=</span><span class="s">'turtle1'</span><span class="p">,</span>       <span class="c1"># 节点改名为 turtle1
</span>            <span class="c1"># IfCondition 会把 "true"/"1" 视为真、"false"/"0" 视为假，直接传参即可
</span>            <span class="n">condition</span><span class="o">=</span><span class="n">IfCondition</span><span class="p">(</span><span class="n">launch_turtlesim1</span><span class="p">),</span>
        <span class="p">),</span>
        <span class="c1"># ---- 第二个节点（改名为 turtle2，与第一个节点区分开）----
</span>        <span class="n">Node</span><span class="p">(</span>
            <span class="n">package</span><span class="o">=</span><span class="s">'turtlesim'</span><span class="p">,</span>
            <span class="n">executable</span><span class="o">=</span><span class="s">'turtlesim_node'</span><span class="p">,</span>
            <span class="n">name</span><span class="o">=</span><span class="s">'turtle2'</span><span class="p">,</span>       <span class="c1"># 节点改名为 turtle2
</span>            <span class="n">condition</span><span class="o">=</span><span class="n">IfCondition</span><span class="p">(</span><span class="n">launch_turtlesim2</span><span class="p">),</span>
        <span class="p">),</span>
    <span class="p">])</span>
</code></pre></div></div>

<blockquote>
  <p><strong>保存与运行</strong>：保存为 <code class="language-plaintext highlighter-rouge">~/ros2_launch_demo/05_condition.launch.py</code>，运行：</p>

  <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 默认启动两个乌龟</span>
ros2 launch ~/ros2_launch_demo/05_condition.launch.py

<span class="c"># 只启动 turtle1，不启动 turtle2</span>
ros2 launch ~/ros2_launch_demo/05_condition.launch.py launch_turtlesim2:<span class="o">=</span><span class="nb">false</span>
</code></pre></div>  </div>
</blockquote>

<hr />

<h2 id="七运行-launch-文件">七、运行 launch 文件</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 基本运行（以第五节的多参数版为例）</span>
ros2 launch ~/ros2_launch_demo/04_param.launch.py

<span class="c"># 覆盖启动参数（对应第五节）</span>
ros2 launch ~/ros2_launch_demo/04_param.launch.py background_r:<span class="o">=</span>200

<span class="c"># 查看可用的启动参数及其默认值/描述</span>
ros2 launch ~/ros2_launch_demo/04_param.launch.py <span class="nt">--show-args</span>
</code></pre></div></div>

<hr />

<h2 id="八常用字段速查表">八、常用字段速查表</h2>

<table>
  <thead>
    <tr>
      <th>字段</th>
      <th>说明</th>
      <th>示例</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">package</code></td>
      <td>功能包名（必填）</td>
      <td><code class="language-plaintext highlighter-rouge">'turtlesim'</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">executable</code></td>
      <td>可执行文件名（必填）</td>
      <td><code class="language-plaintext highlighter-rouge">'turtlesim_node'</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">name</code></td>
      <td>节点名（重命名）</td>
      <td><code class="language-plaintext highlighter-rouge">'my_node'</code> → <code class="language-plaintext highlighter-rouge">/ns1/my_node</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">namespace</code></td>
      <td>命名空间（隔离多实例）</td>
      <td><code class="language-plaintext highlighter-rouge">'ns1'</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">parameters</code></td>
      <td>参数列表</td>
      <td><code class="language-plaintext highlighter-rouge">[{'key': value}]</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">remappings</code></td>
      <td>话题重映射</td>
      <td><code class="language-plaintext highlighter-rouge">[('/旧', '/新')]</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">arguments</code></td>
      <td>透传给节点的额外 ROS 命令行参数（需以 <code class="language-plaintext highlighter-rouge">--ros-args</code> 开头，如设置日志级别/加载参数文件）</td>
      <td><code class="language-plaintext highlighter-rouge">['--ros-args', '--log-level', 'DEBUG']</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">output</code></td>
      <td>日志输出位置</td>
      <td><code class="language-plaintext highlighter-rouge">'screen'</code> | <code class="language-plaintext highlighter-rouge">'log'</code> | <code class="language-plaintext highlighter-rouge">'both'</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">condition</code></td>
      <td>条件启动</td>
      <td><code class="language-plaintext highlighter-rouge">IfCondition(LaunchConfiguration(...))</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">respawn</code></td>
      <td>节点退出后自动重启</td>
      <td><code class="language-plaintext highlighter-rouge">True</code> | <code class="language-plaintext highlighter-rouge">False</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">respawn_delay</code></td>
      <td>重启前的等待秒数</td>
      <td><code class="language-plaintext highlighter-rouge">3.0</code></td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>关于 <code class="language-plaintext highlighter-rouge">executable</code> 名字的来源</strong>：<code class="language-plaintext highlighter-rouge">executable</code> 填的是节点程序的可执行文件名，它取决于节点包的类型：</p>

  <ul>
    <li><strong>C++ 包</strong>（<code class="language-plaintext highlighter-rouge">ament_cmake</code>）：在 <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> 中用 <code class="language-plaintext highlighter-rouge">add_executable()</code> 定义并安装；</li>
    <li><strong>Python 包</strong>（<code class="language-plaintext highlighter-rouge">ament_python</code>）：在 <code class="language-plaintext highlighter-rouge">setup.py</code> 的 <code class="language-plaintext highlighter-rouge">console_scripts</code> 中定义入口点。</li>
  </ul>

  <p>本文统一使用 turtlesim 包自带的 C++ 节点程序 <code class="language-plaintext highlighter-rouge">turtlesim_node</code>，所以直接填 <code class="language-plaintext highlighter-rouge">'turtlesim_node'</code> 即可。</p>
</blockquote>

<hr />

<h2 id="九如何让功能包安装-launch-文件">九、如何让功能包安装 launch 文件</h2>

<p>上面的例子都是在某个已有的功能包里写 launch 文件。如果你在<strong>自己的 Python 功能包</strong>（<code class="language-plaintext highlighter-rouge">ament_python</code>）里放了 launch 文件，还需要在 <code class="language-plaintext highlighter-rouge">setup.py</code> 里<strong>声明安装规则</strong>，否则 <code class="language-plaintext highlighter-rouge">ros2 launch</code> 会报”文件找不到”（launch 文件没有被复制进 <code class="language-plaintext highlighter-rouge">install/</code> 目录）。</p>

<p>在 <code class="language-plaintext highlighter-rouge">setup.py</code> 中增加以下配置（这是<strong>最关键的一行</strong>）：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">os</span>                        <span class="c1"># 用于拼接安装路径
</span><span class="kn">from</span> <span class="nn">glob</span> <span class="kn">import</span> <span class="n">glob</span>            <span class="c1"># 用于匹配 launch 目录下的文件
</span><span class="kn">from</span> <span class="nn">setuptools</span> <span class="kn">import</span> <span class="n">setup</span>     <span class="c1"># 打包配置
</span>
<span class="n">package_name</span> <span class="o">=</span> <span class="s">'my_package'</span>      <span class="c1"># 功能包名（与目录名一致）
</span>
<span class="n">setup</span><span class="p">(</span>
    <span class="c1"># 其他配置参数 ...
</span>    <span class="n">data_files</span><span class="o">=</span><span class="p">[</span>
        <span class="c1"># 其他需要安装的数据文件 ...
</span>        <span class="c1"># 安装所有 launch 文件（最关键的一行）
</span>        <span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="s">'share'</span><span class="p">,</span> <span class="n">package_name</span><span class="p">),</span> <span class="n">glob</span><span class="p">(</span><span class="s">'launch/*.launch.py'</span><span class="p">))</span>
    <span class="p">]</span>
<span class="p">)</span>
</code></pre></div></div>

<p>要点说明：</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">glob('launch/*.launch.py')</code></strong>：把 <code class="language-plaintext highlighter-rouge">launch/</code> 目录下<strong>所有以 <code class="language-plaintext highlighter-rouge">.launch.py</code> 结尾</strong>的文件收集起来（注意是<strong>点</strong> <code class="language-plaintext highlighter-rouge">.launch</code>，不是下划线 <code class="language-plaintext highlighter-rouge">_launch</code>）。</li>
  <li><strong><code class="language-plaintext highlighter-rouge">os.path.join('share', package_name)</code></strong>：安装目标路径，即 <code class="language-plaintext highlighter-rouge">share/&lt;包名&gt;/</code> 下，<code class="language-plaintext highlighter-rouge">ros2 launch</code> 会去那里查找 launch 文件。</li>
  <li>配置好后<strong>必须重新构建</strong>并 source 工作区：</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/ros_ws
colcon build <span class="nt">--packages-select</span> my_package
<span class="nb">source install</span>/setup.bash
ros2 launch my_package my_launch.launch.py
</code></pre></div></div>

<h3 id="91-c-包ament_cmake的调整">9.1 C++ 包（<code class="language-plaintext highlighter-rouge">ament_cmake</code>）的调整</h3>

<p>如果你用的是 <strong>C++ 功能包</strong>（构建类型为 <code class="language-plaintext highlighter-rouge">ament_cmake</code>），则在 <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> 中通过 <code class="language-plaintext highlighter-rouge">install()</code> 命令把 <code class="language-plaintext highlighter-rouge">launch/</code> 目录整体安装到 <code class="language-plaintext highlighter-rouge">share/&lt;包名&gt;/</code> 下。在文件<strong>末尾、<code class="language-plaintext highlighter-rouge">ament_package()</code> 之前</strong>加上：</p>

<div class="language-cmake highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 安装 launch 目录到 share/&lt;包名&gt;/launch</span>
<span class="nb">install</span><span class="p">(</span>DIRECTORY launch
  DESTINATION share/<span class="si">${</span><span class="nv">PROJECT_NAME</span><span class="si">}</span>
<span class="p">)</span>
</code></pre></div></div>

<p>也可以用 <code class="language-plaintext highlighter-rouge">install(FILES ...)</code> 逐个指定（launch 文件不多时更清晰）：</p>

<div class="language-cmake highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">install</span><span class="p">(</span>FILES
  launch/my_launch.launch.py
  DESTINATION share/<span class="si">${</span><span class="nv">PROJECT_NAME</span><span class="si">}</span>/launch
<span class="p">)</span>
</code></pre></div></div>

<p>要点说明：</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">DESTINATION share/${PROJECT_NAME}</code></strong>：安装目标路径，<code class="language-plaintext highlighter-rouge">${PROJECT_NAME}</code> 会自动替换成包名（即 <code class="language-plaintext highlighter-rouge">project(...)</code> 里定义的名字），与 Python 包的 <code class="language-plaintext highlighter-rouge">os.path.join('share', package_name)</code> 等价。</li>
  <li><code class="language-plaintext highlighter-rouge">install(DIRECTORY launch ...)</code> 会把<strong>整个 launch 目录</strong>（含里面的所有文件）复制过去，是最省事的方式；<code class="language-plaintext highlighter-rouge">install(FILES ...)</code> 则只装你列出的那几份。</li>
  <li>同样，改完 <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> 后要<strong>重新构建</strong>：</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/ros_ws
colcon build <span class="nt">--packages-select</span> my_package
<span class="nb">source install</span>/setup.bash
ros2 launch my_package my_launch.launch.py
</code></pre></div></div>

<blockquote>
  <p>小结：无论 Python 还是 C++ 包，本质都是把 <code class="language-plaintext highlighter-rouge">launch/*.launch.py</code> 装进 <code class="language-plaintext highlighter-rouge">install/&lt;包名&gt;/share/&lt;包名&gt;/launch/</code>。Python 包在 <code class="language-plaintext highlighter-rouge">setup.py</code> 的 <code class="language-plaintext highlighter-rouge">data_files</code> 里配置，C++ 包在 <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> 的 <code class="language-plaintext highlighter-rouge">install()</code> 里配置。</p>
</blockquote>

<blockquote>
  <p><strong>常见坑</strong>：</p>

  <ul>
    <li><strong>文件名命名</strong>：launch 文件建议统一命名为 <code class="language-plaintext highlighter-rouge">xxx.launch.py</code>（带点）。如果写成 <code class="language-plaintext highlighter-rouge">xxx_launch.py</code>（下划线），<code class="language-plaintext highlighter-rouge">glob('launch/*.launch.py')</code> 匹配不到，文件不会被安装，<code class="language-plaintext highlighter-rouge">ros2 launch</code> 同样报”文件找不到”。</li>
    <li><strong>忘了重新构建</strong>：修改 <code class="language-plaintext highlighter-rouge">setup.py</code> 后要重新 <code class="language-plaintext highlighter-rouge">colcon build</code>，否则 <code class="language-plaintext highlighter-rouge">install/</code> 里还是旧内容。</li>
    <li><strong>忘了 <code class="language-plaintext highlighter-rouge">package.xml</code> 依赖</strong>：如果 launch 文件里用了 <code class="language-plaintext highlighter-rouge">launch_ros.actions.Node</code>，记得在 <code class="language-plaintext highlighter-rouge">package.xml</code> 加 <code class="language-plaintext highlighter-rouge">&lt;exec_depend&gt;ros2launch&lt;/exec_depend&gt;</code>。</li>
  </ul>
</blockquote>

<hr />

<h2 id="十一个-launch-文件调用另一个-launch-文件并传递参数">十、一个 launch 文件调用另一个 launch 文件并传递参数</h2>

<p>实际项目中，一个 launch 文件常常需要<strong>调用另一个 launch 文件</strong>（例如先启动公共的机器人驱动、传感器驱动），并且把当前文件的参数<strong>传递</strong>给它。这用 <code class="language-plaintext highlighter-rouge">IncludeLaunchDescription</code> + <code class="language-plaintext highlighter-rouge">launch_arguments</code> 就能实现。</p>

<p>下面用一个完整例子演示：<strong>父 launch 文件</strong> 调用 <strong>子 launch 文件</strong>，并把背景色参数传过去。两个文件都放在同一个目录（如 <code class="language-plaintext highlighter-rouge">~/ros2_launch_demo/</code>）下。</p>

<h3 id="101-子-launch-文件声明参数并使用">10.1 子 launch 文件：声明参数并使用</h3>

<p>先写子文件 <code class="language-plaintext highlighter-rouge">child.launch.py</code>：它声明一组参数，并用 <code class="language-plaintext highlighter-rouge">LaunchConfiguration</code> 把参数值应用到 <code class="language-plaintext highlighter-rouge">turtlesim</code> 节点上。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 子 launch 文件：child.launch.py
# 作用：声明一组参数，并用 LaunchConfiguration 把参数值传给节点
</span><span class="kn">from</span> <span class="nn">launch</span> <span class="kn">import</span> <span class="n">LaunchDescription</span>
<span class="kn">from</span> <span class="nn">launch.actions</span> <span class="kn">import</span> <span class="n">DeclareLaunchArgument</span>       <span class="c1"># 定义可配置参数
</span><span class="kn">from</span> <span class="nn">launch.substitutions</span> <span class="kn">import</span> <span class="n">LaunchConfiguration</span>   <span class="c1"># 获取参数值
</span><span class="kn">from</span> <span class="nn">launch_ros.actions</span> <span class="kn">import</span> <span class="n">Node</span>

<span class="k">def</span> <span class="nf">generate_launch_description</span><span class="p">():</span>
    <span class="c1"># 1. 声明三个可配置参数，并给出默认值
</span>    <span class="n">declare_bg_r</span> <span class="o">=</span> <span class="n">DeclareLaunchArgument</span><span class="p">(</span>
        <span class="s">'background_r'</span><span class="p">,</span>          <span class="c1"># 参数名称
</span>        <span class="n">default_value</span><span class="o">=</span><span class="s">'150'</span><span class="p">,</span>     <span class="c1"># 默认值（必须是字符串）
</span>        <span class="n">description</span><span class="o">=</span><span class="s">'背景色 R 通道 (0-255)'</span><span class="p">,</span>  <span class="c1"># 描述信息
</span>    <span class="p">)</span>
    <span class="n">declare_bg_g</span> <span class="o">=</span> <span class="n">DeclareLaunchArgument</span><span class="p">(</span><span class="s">'background_g'</span><span class="p">,</span> <span class="n">default_value</span><span class="o">=</span><span class="s">'86'</span><span class="p">)</span>
    <span class="n">declare_bg_b</span> <span class="o">=</span> <span class="n">DeclareLaunchArgument</span><span class="p">(</span><span class="s">'background_b'</span><span class="p">,</span> <span class="n">default_value</span><span class="o">=</span><span class="s">'255'</span><span class="p">)</span>

    <span class="c1"># 2. 用 LaunchConfiguration 获取参数的实际值（占位符，运行时才替换）
</span>    <span class="n">bg_r</span> <span class="o">=</span> <span class="n">LaunchConfiguration</span><span class="p">(</span><span class="s">'background_r'</span><span class="p">)</span>
    <span class="n">bg_g</span> <span class="o">=</span> <span class="n">LaunchConfiguration</span><span class="p">(</span><span class="s">'background_g'</span><span class="p">)</span>
    <span class="n">bg_b</span> <span class="o">=</span> <span class="n">LaunchConfiguration</span><span class="p">(</span><span class="s">'background_b'</span><span class="p">)</span>

    <span class="c1"># 3. 把参数值应用到 turtlesim 节点上
</span>    <span class="n">turtlesim_node</span> <span class="o">=</span> <span class="n">Node</span><span class="p">(</span>
        <span class="n">package</span><span class="o">=</span><span class="s">'turtlesim'</span><span class="p">,</span>          <span class="c1"># 节点所在的功能包名
</span>        <span class="n">executable</span><span class="o">=</span><span class="s">'turtlesim_node'</span><span class="p">,</span>  <span class="c1"># 可执行文件名（这里直接用 turtlesim 包自带的节点程序）
</span>        <span class="n">parameters</span><span class="o">=</span><span class="p">[{</span>
            <span class="s">'background_r'</span><span class="p">:</span> <span class="n">bg_r</span><span class="p">,</span>     <span class="c1"># 使用上面获取的参数占位符
</span>            <span class="s">'background_g'</span><span class="p">:</span> <span class="n">bg_g</span><span class="p">,</span>
            <span class="s">'background_b'</span><span class="p">:</span> <span class="n">bg_b</span><span class="p">,</span>
        <span class="p">}],</span>
    <span class="p">)</span>

    <span class="c1"># 4. 组装 LaunchDescription（先声明参数，再放节点）
</span>    <span class="k">return</span> <span class="n">LaunchDescription</span><span class="p">([</span>
        <span class="n">declare_bg_r</span><span class="p">,</span>
        <span class="n">declare_bg_g</span><span class="p">,</span>
        <span class="n">declare_bg_b</span><span class="p">,</span>
        <span class="n">turtlesim_node</span><span class="p">,</span>
    <span class="p">])</span>
</code></pre></div></div>

<p>这个子文件也可以单独运行：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ros2 launch ~/ros2_launch_demo/child.launch.py background_r:<span class="o">=</span>200 background_g:<span class="o">=</span>100 background_b:<span class="o">=</span>50
</code></pre></div></div>

<h3 id="102-父-launch-文件声明参数并透传给子文件">10.2 父 launch 文件：声明参数并透传给子文件</h3>

<p>再写父文件 <code class="language-plaintext highlighter-rouge">parent.launch.py</code>：它<strong>自己先声明一组参数</strong>（这样命令行能直接传参），再通过 <code class="language-plaintext highlighter-rouge">IncludeLaunchDescription</code> <strong>包含</strong>子文件，并用 <code class="language-plaintext highlighter-rouge">launch_arguments</code> 把参数值<strong>透传</strong>给子文件。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 父 launch 文件：parent.launch.py
# 作用：声明参数 → 包含（调用）child.launch.py → 把参数传递给它
</span><span class="kn">import</span> <span class="nn">os</span>  <span class="c1"># 用于拼接文件路径
</span>
<span class="kn">from</span> <span class="nn">launch</span> <span class="kn">import</span> <span class="n">LaunchDescription</span>
<span class="kn">from</span> <span class="nn">launch.actions</span> <span class="kn">import</span> <span class="n">DeclareLaunchArgument</span><span class="p">,</span> <span class="n">IncludeLaunchDescription</span>  <span class="c1"># 声明参数 + 包含另一个 launch 文件
</span><span class="kn">from</span> <span class="nn">launch.launch_description_sources</span> <span class="kn">import</span> <span class="n">PythonLaunchDescriptionSource</span>  <span class="c1"># 指定 Python launch 文件源
</span><span class="kn">from</span> <span class="nn">launch.substitutions</span> <span class="kn">import</span> <span class="n">LaunchConfiguration</span>  <span class="c1"># 获取参数值
</span>
<span class="k">def</span> <span class="nf">generate_launch_description</span><span class="p">():</span>
    <span class="c1"># 1. 定位子 launch 文件的绝对路径
</span>    <span class="c1">#    os.path.dirname(__file__) 返回当前文件所在目录（本文件与 child.launch.py 同目录）
</span>    <span class="n">child_launch</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span>
        <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">dirname</span><span class="p">(</span><span class="n">__file__</span><span class="p">),</span>  <span class="c1"># 当前文件所在目录
</span>        <span class="s">'child.launch.py'</span>           <span class="c1"># 子 launch 文件名
</span>    <span class="p">)</span>

    <span class="c1"># 2. 父文件也声明一组参数（这样命令行可以直接传参给父文件）
</span>    <span class="n">declare_bg_r</span> <span class="o">=</span> <span class="n">DeclareLaunchArgument</span><span class="p">(</span><span class="s">'background_r'</span><span class="p">,</span> <span class="n">default_value</span><span class="o">=</span><span class="s">'150'</span><span class="p">)</span>
    <span class="n">declare_bg_g</span> <span class="o">=</span> <span class="n">DeclareLaunchArgument</span><span class="p">(</span><span class="s">'background_g'</span><span class="p">,</span> <span class="n">default_value</span><span class="o">=</span><span class="s">'86'</span><span class="p">)</span>
    <span class="n">declare_bg_b</span> <span class="o">=</span> <span class="n">DeclareLaunchArgument</span><span class="p">(</span><span class="s">'background_b'</span><span class="p">,</span> <span class="n">default_value</span><span class="o">=</span><span class="s">'255'</span><span class="p">)</span>

    <span class="c1"># 3. 获取参数的实际值（占位符，运行时才替换）
</span>    <span class="n">bg_r</span> <span class="o">=</span> <span class="n">LaunchConfiguration</span><span class="p">(</span><span class="s">'background_r'</span><span class="p">)</span>
    <span class="n">bg_g</span> <span class="o">=</span> <span class="n">LaunchConfiguration</span><span class="p">(</span><span class="s">'background_g'</span><span class="p">)</span>
    <span class="n">bg_b</span> <span class="o">=</span> <span class="n">LaunchConfiguration</span><span class="p">(</span><span class="s">'background_b'</span><span class="p">)</span>

    <span class="c1"># 4. 包含（调用）子 launch 文件，并把父文件的参数值传递给它
</span>    <span class="n">include_child</span> <span class="o">=</span> <span class="n">IncludeLaunchDescription</span><span class="p">(</span>
        <span class="n">PythonLaunchDescriptionSource</span><span class="p">(</span><span class="n">child_launch</span><span class="p">),</span>  <span class="c1"># 指定要包含的 launch 文件
</span>        <span class="n">launch_arguments</span><span class="o">=</span><span class="p">{</span>                            <span class="c1"># 传给子 launch 文件的参数（键值对）
</span>            <span class="s">'background_r'</span><span class="p">:</span> <span class="n">bg_r</span><span class="p">,</span>     <span class="c1"># 把父文件的参数值透传给子文件
</span>            <span class="s">'background_g'</span><span class="p">:</span> <span class="n">bg_g</span><span class="p">,</span>
            <span class="s">'background_b'</span><span class="p">:</span> <span class="n">bg_b</span><span class="p">,</span>
        <span class="p">}.</span><span class="n">items</span><span class="p">(),</span>  <span class="c1"># 注意：launch_arguments 需要 .items() 转成键值对列表
</span>    <span class="p">)</span>

    <span class="c1"># 5. 组装 LaunchDescription（先声明参数，再放 IncludeLaunchDescription）
</span>    <span class="k">return</span> <span class="n">LaunchDescription</span><span class="p">([</span>
        <span class="n">declare_bg_r</span><span class="p">,</span>
        <span class="n">declare_bg_g</span><span class="p">,</span>
        <span class="n">declare_bg_b</span><span class="p">,</span>
        <span class="n">include_child</span><span class="p">,</span>
    <span class="p">])</span>
</code></pre></div></div>

<blockquote>
  <p>说明：子文件里 <code class="language-plaintext highlighter-rouge">DeclareLaunchArgument</code> 声明的参数，父文件<strong>不一定要全部传</strong>。没传的参数会使用子文件里的默认值。</p>
</blockquote>

<h3 id="103-运行">10.3 运行</h3>

<p>两个文件都放在同一个目录下（本示例用 <code class="language-plaintext highlighter-rouge">~/ros2_launch_demo/</code>），直接运行父文件即可：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 运行父文件 → 自动包含子文件，参数默认 150/86/255（青色系）</span>
ros2 launch ~/ros2_launch_demo/parent.launch.py

<span class="c"># 通过命令行给父文件传参 → 父文件透传给子文件 → 背景变红色</span>
ros2 launch ~/ros2_launch_demo/parent.launch.py background_r:<span class="o">=</span>255 background_g:<span class="o">=</span>0 background_b:<span class="o">=</span>0

<span class="c"># 传蓝色</span>
ros2 launch ~/ros2_launch_demo/parent.launch.py background_r:<span class="o">=</span>0 background_g:<span class="o">=</span>0 background_b:<span class="o">=</span>255
</code></pre></div></div>

<p>运行后 turtlesim 窗口的背景色会变成你通过命令行传给父文件的颜色。可以用 <code class="language-plaintext highlighter-rouge">ros2 param get /turtlesim background_r</code> 实时确认参数值。</p>

<blockquote>
  <p><strong>常见坑</strong>：<code class="language-plaintext highlighter-rouge">IncludeLaunchDescription</code> 里的路径必须写成 <strong>绝对路径</strong>。这里用 <code class="language-plaintext highlighter-rouge">os.path.dirname(__file__)</code> 动态获取当前文件所在目录，所以无论把两个文件放到哪里都能正确找到子文件。</p>
</blockquote>

<blockquote>
  <p><strong>小结</strong>：</p>

  <ul>
    <li><strong><code class="language-plaintext highlighter-rouge">IncludeLaunchDescription(PythonLaunchDescriptionSource(路径), launch_arguments={...}.items())</code></strong> 就是”调用另一个 launch 文件”的标准写法。</li>
    <li>父文件<strong>先自己声明参数</strong>（<code class="language-plaintext highlighter-rouge">DeclareLaunchArgument</code>），再用 <code class="language-plaintext highlighter-rouge">LaunchConfiguration</code> 把值塞进 <code class="language-plaintext highlighter-rouge">launch_arguments</code>，从而把命令行参数<strong>透传</strong>给子文件——这就是”调用参数并传递给另一个 launch 文件”。</li>
    <li>子文件通过 <code class="language-plaintext highlighter-rouge">DeclareLaunchArgument</code> + <code class="language-plaintext highlighter-rouge">LaunchConfiguration</code> <strong>接收并使用</strong>参数。</li>
    <li>参数值<strong>必须是字符串</strong>（数字也要写成 <code class="language-plaintext highlighter-rouge">'255'</code> 这样的字符串；<code class="language-plaintext highlighter-rouge">LaunchConfiguration</code> 替换后自动转成字符串）。</li>
    <li>用 <code class="language-plaintext highlighter-rouge">os.path.dirname(__file__)</code> 拿到当前文件所在目录，再拼接出同目录下子 launch 文件的绝对路径，方便快速测试。</li>
    <li>生产项目中，子文件通常安装在功能包里，改用 <code class="language-plaintext highlighter-rouge">get_package_share_directory('包名')</code> 来定位（见第九节）。</li>
  </ul>
</blockquote>

<hr />

<h2 id="十一高级用法执行任意命令executeprocess与定时动作timeraction">十一、高级用法：执行任意命令（ExecuteProcess）与定时动作（TimerAction）</h2>

<p>前几节的 <code class="language-plaintext highlighter-rouge">Node</code> 用来<strong>启动节点</strong>、<code class="language-plaintext highlighter-rouge">DeclareLaunchArgument</code> 用来<strong>声明参数</strong>。但实际项目中还有两类需求：</p>

<ul>
  <li><strong>执行任意命令</strong>：启动后需要跑一条”一次性命令”，比如 <code class="language-plaintext highlighter-rouge">ros2 service call</code> 调用服务、<code class="language-plaintext highlighter-rouge">ros2 param set</code> 设置参数、运行一个脚本。</li>
  <li><strong>延迟执行</strong>：某些动作要<strong>等节点启动完成、或等上一步参数生效</strong>后再执行，而不是一上来就抢跑。</li>
</ul>

<p>这两类需求分别对应 <code class="language-plaintext highlighter-rouge">ExecuteProcess</code>（执行任意命令）和 <code class="language-plaintext highlighter-rouge">TimerAction</code>（定时动作）。两者都是 <code class="language-plaintext highlighter-rouge">launch.actions</code> 里的标准 <strong>Action</strong>，可以直接放进 <code class="language-plaintext highlighter-rouge">LaunchDescription</code>。</p>

<hr />

<h3 id="111-executeprocess--在-launch-中执行任意命令">11.1 ExecuteProcess —— 在 launch 中执行任意命令</h3>

<p><strong>作用</strong>：在 launch 启动时执行任意 shell 命令（不限于 ROS 命令）。</p>

<p><strong>最小示例</strong>：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">launch</span> <span class="kn">import</span> <span class="n">LaunchDescription</span>
<span class="kn">from</span> <span class="nn">launch.actions</span> <span class="kn">import</span> <span class="n">ExecuteProcess</span>

<span class="k">def</span> <span class="nf">generate_launch_description</span><span class="p">():</span>
    <span class="k">return</span> <span class="n">LaunchDescription</span><span class="p">([</span>
        <span class="c1"># 执行一条简单命令
</span>        <span class="n">ExecuteProcess</span><span class="p">(</span>
            <span class="n">cmd</span><span class="o">=</span><span class="p">[</span><span class="s">'echo'</span><span class="p">,</span> <span class="s">'hello from launch'</span><span class="p">],</span>   <span class="c1"># 命令 + 参数，列表形式
</span>        <span class="p">),</span>
    <span class="p">])</span>
</code></pre></div></div>

<p><strong>关键参数</strong>：</p>

<table>
  <thead>
    <tr>
      <th>参数</th>
      <th>说明</th>
      <th>示例</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">cmd</code></td>
      <td>要执行的命令，<strong>字符串列表</strong></td>
      <td><code class="language-plaintext highlighter-rouge">['echo', 'hello']</code> 或 <code class="language-plaintext highlighter-rouge">['ros2', 'node', 'list']</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">shell</code></td>
      <td>是否通过 shell 执行。为 <code class="language-plaintext highlighter-rouge">True</code> 时可使用 <code class="language-plaintext highlighter-rouge">|</code>、<code class="language-plaintext highlighter-rouge">&gt;</code>、<code class="language-plaintext highlighter-rouge">&amp;&amp;</code> 等 shell 语法</td>
      <td><code class="language-plaintext highlighter-rouge">shell=True</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">output</code></td>
      <td>命令输出打印到哪里</td>
      <td><code class="language-plaintext highlighter-rouge">'screen'</code> / <code class="language-plaintext highlighter-rouge">'log'</code> / <code class="language-plaintext highlighter-rouge">'both'</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">condition</code></td>
      <td>条件执行（配合 <code class="language-plaintext highlighter-rouge">IfCondition</code>）</td>
      <td><code class="language-plaintext highlighter-rouge">IfCondition(...)</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">env</code></td>
      <td>为命令设置额外环境变量</td>
      <td><code class="language-plaintext highlighter-rouge">{'MY_VAR': 'value'}</code></td>
    </tr>
  </tbody>
</table>

<p><strong>两种 <code class="language-plaintext highlighter-rouge">cmd</code> 写法</strong>：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 写法 1：普通列表 —— 每个元素是一个参数
</span><span class="n">ExecuteProcess</span><span class="p">(</span>
    <span class="n">cmd</span><span class="o">=</span><span class="p">[</span><span class="s">'ros2'</span><span class="p">,</span> <span class="s">'node'</span><span class="p">,</span> <span class="s">'list'</span><span class="p">],</span>
<span class="p">)</span>

<span class="c1"># 写法 2：嵌套列表 —— 常用于"多个片段拼接"（配合 Substitution 替换）
#   launch 会把内层列表逐段解析，允许混入 LaunchConfiguration 等占位符
</span><span class="n">ExecuteProcess</span><span class="p">(</span>
    <span class="n">cmd</span><span class="o">=</span><span class="p">[[</span>
        <span class="s">'ros2 param set '</span><span class="p">,</span>
        <span class="n">turtlesim_ns</span><span class="p">,</span>          <span class="c1"># 这里可以是 LaunchConfiguration 占位符
</span>        <span class="s">'/sim background_r '</span><span class="p">,</span>
        <span class="s">'120'</span>
    <span class="p">]],</span>
    <span class="n">shell</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>

<blockquote>
  <p><strong>用 <code class="language-plaintext highlighter-rouge">FindExecutable</code> 定位命令（推荐）</strong>：直接写 <code class="language-plaintext highlighter-rouge">'ros2'</code> 依赖 PATH；更健壮的做法是用 <code class="language-plaintext highlighter-rouge">FindExecutable(name='ros2')</code> 动态查找其绝对路径，不依赖 PATH 环境变量：</p>

  <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">launch.substitutions</span> <span class="kn">import</span> <span class="n">FindExecutable</span>

<span class="n">ExecuteProcess</span><span class="p">(</span>
    <span class="n">cmd</span><span class="o">=</span><span class="p">[[</span>
        <span class="n">FindExecutable</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s">'ros2'</span><span class="p">),</span>   <span class="c1"># 自动找到 ros2 的绝对路径
</span>        <span class="s">' service call /spawn turtlesim/srv/Spawn "{x: 2, y: 2, theta: 0.2}"'</span>
    <span class="p">]],</span>
    <span class="n">shell</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div>  </div>
</blockquote>

<hr />

<h3 id="112-timeraction--定时延迟执行动作">11.2 TimerAction —— 定时延迟执行动作</h3>

<p><strong>作用</strong>：把一组动作<strong>延迟 <code class="language-plaintext highlighter-rouge">period</code> 秒后</strong>再执行。适合”等节点就绪后再发指令”的场景。</p>

<p><strong>最小示例</strong>：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">launch</span> <span class="kn">import</span> <span class="n">LaunchDescription</span>
<span class="kn">from</span> <span class="nn">launch.actions</span> <span class="kn">import</span> <span class="n">ExecuteProcess</span><span class="p">,</span> <span class="n">TimerAction</span>

<span class="k">def</span> <span class="nf">generate_launch_description</span><span class="p">():</span>
    <span class="n">delayed_cmd</span> <span class="o">=</span> <span class="n">ExecuteProcess</span><span class="p">(</span>
        <span class="n">cmd</span><span class="o">=</span><span class="p">[</span><span class="s">'echo'</span><span class="p">,</span> <span class="s">'delayed 3 seconds'</span><span class="p">],</span>
    <span class="p">)</span>
    <span class="k">return</span> <span class="n">LaunchDescription</span><span class="p">([</span>
        <span class="c1"># 3 秒后才执行 delayed_cmd
</span>        <span class="n">TimerAction</span><span class="p">(</span>
            <span class="n">period</span><span class="o">=</span><span class="mf">3.0</span><span class="p">,</span>                  <span class="c1"># 延迟秒数（浮点数）
</span>            <span class="n">actions</span><span class="o">=</span><span class="p">[</span><span class="n">delayed_cmd</span><span class="p">],</span>       <span class="c1"># 延迟后要执行的动作列表
</span>        <span class="p">),</span>
    <span class="p">])</span>
</code></pre></div></div>

<p><strong>关键参数</strong>：</p>

<table>
  <thead>
    <tr>
      <th>参数</th>
      <th>说明</th>
      <th>示例</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">period</code></td>
      <td>延迟的秒数（浮点数）</td>
      <td><code class="language-plaintext highlighter-rouge">2.0</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">actions</code></td>
      <td>到点后要执行的动作<strong>列表</strong>（可以是 <code class="language-plaintext highlighter-rouge">ExecuteProcess</code>、<code class="language-plaintext highlighter-rouge">Node</code>、<code class="language-plaintext highlighter-rouge">LogInfo</code> 等）</td>
      <td><code class="language-plaintext highlighter-rouge">[cmd1, cmd2]</code></td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>注意</strong>：<code class="language-plaintext highlighter-rouge">TimerAction</code> 的 <code class="language-plaintext highlighter-rouge">actions</code> 接收的是<strong>动作对象列表</strong>。如果你已经有 <code class="language-plaintext highlighter-rouge">ExecuteProcess</code> / <code class="language-plaintext highlighter-rouge">Node</code> 等动作变量，直接把它们放进来即可。<code class="language-plaintext highlighter-rouge">period</code> 可以用浮点（如 <code class="language-plaintext highlighter-rouge">0.5</code>）。</p>
</blockquote>

<hr />

<h3 id="113-综合示例07_example_substitutionslaunchpy">11.3 综合示例：<code class="language-plaintext highlighter-rouge">07_example_substitutions.launch.py</code></h3>

<p>下面把上面两类用法和前面的知识（参数替换、条件启动）组合起来，完成一个<strong>自动演示</strong>：启动 turtlesim → 生成海龟 → 改背景色（其中一步延迟 2 秒且带条件）。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 子 launch 文件：07_example_substitutions.launch.py
# 作用：演示 launch 文件的高级用法 —— 参数替换（Substitutions）、
#       执行任意命令（ExecuteProcess）、条件启动（IfCondition）、定时动作（TimerAction）
</span>
<span class="c1"># 用于创建节点（Node）
</span><span class="kn">from</span> <span class="nn">launch_ros.actions</span> <span class="kn">import</span> <span class="n">Node</span>

<span class="c1"># launch 基础
</span><span class="kn">from</span> <span class="nn">launch</span> <span class="kn">import</span> <span class="n">LaunchDescription</span>
<span class="c1"># 声明启动参数 + 执行任意命令 + 定时延迟动作
</span><span class="kn">from</span> <span class="nn">launch.actions</span> <span class="kn">import</span> <span class="n">DeclareLaunchArgument</span><span class="p">,</span> <span class="n">ExecuteProcess</span><span class="p">,</span> <span class="n">TimerAction</span>
<span class="c1"># 条件启动：满足条件才执行某个动作
</span><span class="kn">from</span> <span class="nn">launch.conditions</span> <span class="kn">import</span> <span class="n">IfCondition</span>
<span class="c1"># 参数替换：获取启动参数值 + 计算 Python 表达式
</span><span class="kn">from</span> <span class="nn">launch.substitutions</span> <span class="kn">import</span> <span class="n">LaunchConfiguration</span><span class="p">,</span> <span class="n">PythonExpression</span>


<span class="k">def</span> <span class="nf">generate_launch_description</span><span class="p">():</span>
    <span class="c1"># ---- 1. 用 LaunchConfiguration 获取启动参数的值（占位符，运行时才替换）----
</span>    <span class="n">turtlesim_ns</span> <span class="o">=</span> <span class="n">LaunchConfiguration</span><span class="p">(</span><span class="s">'turtlesim_ns'</span><span class="p">)</span>            <span class="c1"># 命名空间
</span>    <span class="n">use_provided_red</span> <span class="o">=</span> <span class="n">LaunchConfiguration</span><span class="p">(</span><span class="s">'use_provided_red'</span><span class="p">)</span>    <span class="c1"># 是否使用命令行传入的红色值
</span>    <span class="n">new_background_r</span> <span class="o">=</span> <span class="n">LaunchConfiguration</span><span class="p">(</span><span class="s">'new_background_r'</span><span class="p">)</span>    <span class="c1"># 新的背景色 R 值
</span>
    <span class="c1"># ---- 2. 声明三个可配置的启动参数，并给出默认值 ----
</span>    <span class="c1">#    注意：必须先声明，才能在命令行用 参数名:=值 覆盖
</span>    <span class="n">turtlesim_ns_launch_arg</span> <span class="o">=</span> <span class="n">DeclareLaunchArgument</span><span class="p">(</span>
        <span class="s">'turtlesim_ns'</span><span class="p">,</span>               <span class="c1"># 参数名称
</span>        <span class="n">default_value</span><span class="o">=</span><span class="s">'turtlesim1'</span>    <span class="c1"># 默认值（必须是字符串）
</span>    <span class="p">)</span>
    <span class="n">use_provided_red_launch_arg</span> <span class="o">=</span> <span class="n">DeclareLaunchArgument</span><span class="p">(</span>
        <span class="s">'use_provided_red'</span><span class="p">,</span>
        <span class="n">default_value</span><span class="o">=</span><span class="s">'False'</span>
    <span class="p">)</span>
    <span class="n">new_background_r_launch_arg</span> <span class="o">=</span> <span class="n">DeclareLaunchArgument</span><span class="p">(</span>
        <span class="s">'new_background_r'</span><span class="p">,</span>
        <span class="n">default_value</span><span class="o">=</span><span class="s">'200'</span>
    <span class="p">)</span>

    <span class="c1"># ---- 3. 创建 turtlesim 节点，并放入指定命名空间 ----
</span>    <span class="c1">#    namespace 用 LaunchConfiguration 替换 → 节点名由命令行参数决定
</span>    <span class="n">turtlesim_node</span> <span class="o">=</span> <span class="n">Node</span><span class="p">(</span>
        <span class="n">package</span><span class="o">=</span><span class="s">'turtlesim'</span><span class="p">,</span>            <span class="c1"># 节点所在的功能包名
</span>        <span class="n">namespace</span><span class="o">=</span><span class="n">turtlesim_ns</span><span class="p">,</span>         <span class="c1"># 命名空间（命令行传 turtlesim_ns:=xxx 可覆盖）
</span>        <span class="n">executable</span><span class="o">=</span><span class="s">'turtlesim_node'</span><span class="p">,</span>    <span class="c1"># 可执行文件名
</span>        <span class="n">name</span><span class="o">=</span><span class="s">'sim'</span>                      <span class="c1"># 节点名 → 全名变成 /&lt;namespace&gt;/sim
</span>    <span class="p">)</span>

    <span class="c1"># ---- 4. ExecuteProcess：在 launch 中直接执行任意 shell 命令 ----
</span>    <span class="c1">#    在 turtlesim 中生成一只新海龟（调用 /spawn 服务）
</span>    <span class="n">spawn_turtle</span> <span class="o">=</span> <span class="n">ExecuteProcess</span><span class="p">(</span>
        <span class="c1"># cmd 是一个字符串列表，逐段拼接成完整命令
</span>        <span class="n">cmd</span><span class="o">=</span><span class="p">[[</span>
            <span class="s">'ros2 service call '</span><span class="p">,</span>                        <span class="c1"># 调用服务命令
</span>            <span class="n">turtlesim_ns</span><span class="p">,</span>                                <span class="c1"># 命名空间（替换成实际值）
</span>            <span class="s">'/spawn '</span><span class="p">,</span>                                   <span class="c1"># 服务名
</span>            <span class="s">'turtlesim/srv/Spawn '</span><span class="p">,</span>                      <span class="c1"># 服务类型
</span>            <span class="s">'"{x: 2, y: 2, theta: 0.2}"'</span>                 <span class="c1"># 服务参数（YAML 格式）
</span>        <span class="p">]],</span>
        <span class="n">shell</span><span class="o">=</span><span class="bp">True</span>                      <span class="c1"># 通过 shell 执行（允许重定向、管道等）
</span>    <span class="p">)</span>

    <span class="c1">#    无条件修改背景色 R 为 120
</span>    <span class="n">change_background_r</span> <span class="o">=</span> <span class="n">ExecuteProcess</span><span class="p">(</span>
        <span class="n">cmd</span><span class="o">=</span><span class="p">[[</span>
            <span class="s">'ros2 param set '</span><span class="p">,</span>           <span class="c1"># 设置参数命令
</span>            <span class="n">turtlesim_ns</span><span class="p">,</span>                <span class="c1"># 命名空间（替换成实际值）
</span>            <span class="s">'/sim background_r '</span><span class="p">,</span>        <span class="c1"># 节点/参数名
</span>            <span class="s">'120'</span>                        <span class="c1"># 要设置的参数值
</span>        <span class="p">]],</span>
        <span class="n">shell</span><span class="o">=</span><span class="bp">True</span>
    <span class="p">)</span>

    <span class="c1">#    条件修改背景色 R 为命令行传入的值（仅在满足条件时执行）
</span>    <span class="n">change_background_r_conditioned</span> <span class="o">=</span> <span class="n">ExecuteProcess</span><span class="p">(</span>
        <span class="c1"># ---- 5. IfCondition + PythonExpression：根据表达式结果决定是否执行 ----
</span>        <span class="c1">#    PythonExpression 会把各片段拼成表达式求值（如 "200 == 200 and True"）
</span>        <span class="n">condition</span><span class="o">=</span><span class="n">IfCondition</span><span class="p">(</span>
            <span class="n">PythonExpression</span><span class="p">([</span>
                <span class="n">new_background_r</span><span class="p">,</span>          <span class="c1"># 值 1（如 "200"）
</span>                <span class="s">' == 200'</span><span class="p">,</span>                 <span class="c1"># 比较
</span>                <span class="s">' and '</span><span class="p">,</span>                   <span class="c1"># 逻辑与
</span>                <span class="n">use_provided_red</span>           <span class="c1"># 值 2（如 "True"/"False"）
</span>            <span class="p">])</span>
        <span class="p">),</span>
        <span class="n">cmd</span><span class="o">=</span><span class="p">[[</span>
            <span class="s">'ros2 param set '</span><span class="p">,</span>           <span class="c1"># 设置参数命令
</span>            <span class="n">turtlesim_ns</span><span class="p">,</span>                <span class="c1"># 命名空间（替换成实际值）
</span>            <span class="s">'/sim background_r '</span><span class="p">,</span>        <span class="c1"># 节点/参数名
</span>            <span class="n">new_background_r</span>             <span class="c1"># 命令行传入的新值（替换成实际值）
</span>        <span class="p">]],</span>
        <span class="n">shell</span><span class="o">=</span><span class="bp">True</span>
    <span class="p">)</span>

    <span class="c1"># ---- 6. 组装 LaunchDescription（先声明参数，再放节点和动作）----
</span>    <span class="k">return</span> <span class="n">LaunchDescription</span><span class="p">([</span>
        <span class="c1"># 声明启动参数（必须放在使用它们的动作之前）
</span>        <span class="n">turtlesim_ns_launch_arg</span><span class="p">,</span>
        <span class="n">use_provided_red_launch_arg</span><span class="p">,</span>
        <span class="n">new_background_r_launch_arg</span><span class="p">,</span>
        <span class="c1"># 启动 turtlesim 节点
</span>        <span class="n">turtlesim_node</span><span class="p">,</span>
        <span class="c1"># 生成新海龟
</span>        <span class="n">spawn_turtle</span><span class="p">,</span>
        <span class="c1"># 无条件把背景色 R 设为 120
</span>        <span class="n">change_background_r</span><span class="p">,</span>
        <span class="c1"># ---- 7. TimerAction：延迟 2 秒后再执行条件修改背景色 ----
</span>        <span class="c1">#    保证前面步骤执行完、参数设置生效后再修改
</span>        <span class="n">TimerAction</span><span class="p">(</span>
            <span class="n">period</span><span class="o">=</span><span class="mf">2.0</span><span class="p">,</span>                              <span class="c1"># 延迟 2 秒
</span>            <span class="n">actions</span><span class="o">=</span><span class="p">[</span><span class="n">change_background_r_conditioned</span><span class="p">]</span>  <span class="c1"># 延迟后要执行的动作列表
</span>        <span class="p">)</span>
    <span class="p">])</span>
</code></pre></div></div>

<p><strong>运行</strong>（保存为 <code class="language-plaintext highlighter-rouge">~/ros2_launch_demo/07_example_substitutions.launch.py</code>）：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 默认：命名空间 turtlesim1，不改背景色</span>
ros2 launch ~/ros2_launch_demo/07_example_substitutions.launch.py

<span class="c"># 命令行覆盖：命名空间 turtlesim3 + 满足条件（200 == 200 and True）→ 延迟 2 秒后背景色 R 设为 200</span>
ros2 launch ~/ros2_launch_demo/07_example_substitutions.launch.py <span class="se">\</span>
    turtlesim_ns:<span class="o">=</span><span class="s1">'turtlesim3'</span> use_provided_red:<span class="o">=</span><span class="s1">'True'</span> new_background_r:<span class="o">=</span>200
</code></pre></div></div>

<p><strong>执行时序</strong>（以第二行命令为例）：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>t=0s    启动 turtlesim 节点（/turtlesim3/sim）
t≈0s    调用 /spawn 生成新海龟（x=2, y=2）
t≈0s    ros2 param set /turtlesim3/sim background_r 120   ← 无条件先设成 120
t=2s    （TimerAction 到期）条件成立 → 再设为 200          ← 覆盖成命令行传入的值
</code></pre></div></div>

<blockquote>
  <p><strong>小结</strong>：</p>

  <ul>
    <li><strong><code class="language-plaintext highlighter-rouge">ExecuteProcess(cmd=列表, shell=True)</code></strong>：在 launch 里执行任意命令。<code class="language-plaintext highlighter-rouge">cmd</code> 可以是普通列表或嵌套列表；嵌套列表里可混入 <code class="language-plaintext highlighter-rouge">LaunchConfiguration</code> 等占位符实现动态拼命令。</li>
    <li><strong><code class="language-plaintext highlighter-rouge">TimerAction(period=秒, actions=[...])</code></strong>：把一组动作延迟指定秒数再执行，常用于”等节点就绪后再操作”。</li>
    <li><strong><code class="language-plaintext highlighter-rouge">FindExecutable(name='xxx')</code></strong>：动态查找命令的绝对路径，比硬编码命令名更健壮。</li>
    <li>两者都是普通 Action，可自由与 <code class="language-plaintext highlighter-rouge">DeclareLaunchArgument</code>、<code class="language-plaintext highlighter-rouge">LaunchConfiguration</code>、<code class="language-plaintext highlighter-rouge">IfCondition</code>、<code class="language-plaintext highlighter-rouge">PythonExpression</code> 组合，实现复杂的自动化启动流程。</li>
  </ul>
</blockquote>]]></content><author><name>老张同志</name></author><category term="ros2" /><category term="tutorial" /><summary type="html"><![CDATA[从零掌握 ROS 2 Python launch 文件：核心概念、基础启动、多节点隔离、启动参数、条件启动、launch 安装配置、launch 互相调用、高级用法（ExecuteProcess / TimerAction），附常用字段速查表。]]></summary></entry></feed>