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 / urdfdom | urdf::Model parse | ○ parse のみ、listener 無し |
kdl_parser | URDF → KDL::Tree | 変換だけ |
robot_state_publisher | URDF 受けて /tf 送出 | 送信側 |
moveit RobotModelLoader | URDF ロード | 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_descriptiontopic を 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::Modelstd_msgs—Stringrclcpp
すべて 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直接で十分