notes
Tools

URDF listener パターン (tf2::Buffer + Listener の URDF 版)

URDF 情報を複数 node で共有する時の pattern。/robot_description を transient_local で subscribe して urdf::Model に parse、query API を提供する Buffer + Listener を作る

要点: URDF joint limit / link inertia などを複数 node で共有したい時、各 node で urdf::Model を parse する重複を避ける。tf2 の Buffer + TransformListener と同じパターンで、RobotDescriptionBuffer + Listener を作れば URDF-listener として再利用できる。ROS 2 標準ライブラリには相当するものが無いので、自作するのが実用解。

なぜ必要か#

URDF 情報を使う場面:

  • motor_driver — 関節可動範囲を position clamp に
  • homing — over-travel detection に
  • motor_calibrator — 安全域内で動かすため
  • 経路生成 — 機体外形・関節上限
  • HSM — named position の妥当性検査

各 node が urdf::Model を個別に initString() すると、URDF 文字列の入手経路と parse コードが N 箇所に重複する。tf2 が transform をまとめて管理しているのと同じ問題。

標準ライブラリの現状 (2026)#

ROS 2 ecosystem を survey しても汎用 URDF listener は存在しない:

ライブラリ何を提供listener 相当か
urdf / urdfdomurdf::Model parse○ parse のみ、listener 無し
kdl_parserURDF → KDL::Tree変換だけ
robot_state_publisherURDF 受けて /tf 送出送信側
moveit RobotModelLoaderURDF ロードMoveIt 依存 (重い)

各プロジェクトが再実装している状態。rviz2 の robot_model_display が内部で /robot_description subscribe → parse をやっているのが最も近い pattern。

Pattern: Buffer + Listener 分離#

tf2 の Buffer + TransformListener に倣う。subscribe と保存を分離するのがミソ:

  • Buffer: 内部に urdf::Model を保持、query API を提供。subscribe しない
  • Listener: /robot_description topic を subscribe、届いた文字列を Buffer に流し込む

分離の利点:

  • テスト容易: gtest で buffer.set_from_string("<robot>...") と直接呼べる (subscribe 不要)
  • 拡張性: 将来 URDF 以外 (SRDF, custom XML) を feed する別 Listener を足せる
  • 既知パターン: tf2 触ったことある人はすぐ理解

実装 skeleton#

namespace <ns>::urdf_utils {

class RobotDescriptionBuffer {
public:
  bool set_from_string(const std::string & urdf_xml);  // Listener が呼ぶ
  bool is_ready() const;

  // Query 例 (すべて std::optional で返す, joint 不在時は nullopt)
  std::optional<JointLimits> joint_limits(const std::string & name) const;
  std::optional<Pose>        joint_origin(const std::string & name) const;
  std::optional<LinkInertia> link_inertia(const std::string & name) const;

  std::vector<std::string> joint_names() const;

  using UpdateCallback = std::function<void()>;
  void add_on_update(UpdateCallback cb);  // 再 publish 追従用

private:
  mutable std::mutex mutex_;
  std::shared_ptr<urdf::Model> model_;
};

class RobotDescriptionListener {
public:
  RobotDescriptionListener(
    RobotDescriptionBuffer & buffer,
    rclcpp::Node * node,
    const std::string & topic = "/robot_description");
  // 内部で subscribe → buffer.set_from_string
};

// tf2::waitForTransform 相当
bool wait_for_urdf(
  RobotDescriptionBuffer & buffer,
  rclcpp::Node * node,
  std::chrono::milliseconds timeout);

}  // namespace

使う側 (最小 3 行)#

class MyNode : public rclcpp::Node {
  RobotDescriptionBuffer urdf_buf_;
  RobotDescriptionListener urdf_listener_{urdf_buf_, this};

public:
  void on_startup() {
    if (!wait_for_urdf(urdf_buf_, this, std::chrono::seconds(5))) return;
    auto lim = urdf_buf_.joint_limits("front_lift");
    if (lim) motor_->setPositionMax(lim->upper);
  }
};

QoS の注意点#

/robot_description topic は robot_state_publisher が transient_local + reliable + KeepLast(1) で publish する。Listener も同じ QoS で subscribe しないと後発の subscriber が受信できない:

auto qos = rclcpp::QoS(1).reliable().transient_local();
sub_ = node->create_subscription<std_msgs::msg::String>(topic, qos, cb, opts);

これが違うと「起動順序で timing が悪いと URDF 来ない」の症状になる。

起動時 race 対応#

Subscribe が完了して初回受信するまで数十 ms かかる。それまで query は nullopt を返す。3 対処:

A. wait_for_urdf で blocking 待機#

tf2::waitForTransform と同じ思想。node 内で spin_some を回して timeout まで待つ。推奨。

B. add_on_update callback で非同期処理#

URDF 到着で init を継続。lifecycle node と組みやすいが、状態管理が複雑化。

C. 起動後の polling#

タイマで is_ready() を確認。単純だが起動遅延の原因になりがち。

URDF 更新への追従#

robot_state_publisher は通常 1 度しか publish しないが、xacro 再展開などで再 publish されるケースはある。transient_local なので subscribe 側は毎回 callback が呼ばれる。Buffer 内で mutex 保護しつつ model_ を差し替え、add_on_update で通知した callback 群を lock 外で invoke するのが安全:

void notify_update() const {
  std::vector<UpdateCallback> cbs;
  {
    std::lock_guard<std::mutex> lock(mutex_);
    cbs = callbacks_;
  }
  for (const auto & cb : cbs) cb();  // lock 外で
}

依存#

  • urdf (urdfdom) — urdf::Model
  • std_msgs — String
  • rclcpp

すべて ROS 2 標準 install に含まれる。

テスト#

Buffer は subscribe 依存が無いので単独 test 可:

RobotDescriptionBuffer buf;
ASSERT_TRUE(buf.set_from_string(R"(<robot name="t"><joint name="a" type="prismatic">
  <parent link="b"/><child link="c"/>
  <limit lower="0" upper="0.5" effort="10" velocity="1"/>
</joint><link name="b"/><link name="c"/></robot>)"));
EXPECT_EQ(buf.joint_limits("a")->upper, 0.5);

Listener の integration test は mock publisher で:

auto pub = node->create_publisher<std_msgs::msg::String>(
  "/robot_description",
  rclcpp::QoS(1).reliable().transient_local());
pub->publish(msg);
ASSERT_TRUE(wait_for_urdf(buf, node, 1s));

関連#

  • tf2::Buffer + TransformListener — 元となる pattern
  • URDF の関節情報を複数 node で使うには本 pattern。1 node 内なら urdf::Model 直接で十分
最終更新:

On this page