はじめに
ROS 2 Advent Calendar 19日目の記事です。
本記事では、ROS 2メッセージとJSONデータの相互変換機能を提供するC++ライブラリである json_converter_cpp を紹介します。Pythonには既に類似のライブラリ rclpy_message_converter がありますが、本ライブラリはC++環境で、ROS 2システムと外部データ形式との連携や、メッセージデータの永続化などを可能にするために開発しました。
C++でROS 2メッセージをJSONで扱うための他のアプローチとして、以下のプロジェクトも参考になります。
使い方
ビルド方法
json_converter_cpp は、標準的なROS 2パッケージとして構成されています。ビルドには、ROS 2の依存関係に加え、JSON処理ライブラリとして RapidJSON が必要です。
使い方サンプル (C++)
ROS 2メッセージとJSONを相互変換するC++での基本的な組み込み方法を、型が既知の場合と実行時に決まる場合に分けて示します。
1. 型がコンパイル時に既知の場合
標準のROS 2メッセージ型(例: std_msgs::msg::String)を直接扱う方法です。
#include "json_converter_cpp/converter.hpp"
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <iostream>
#include <std_msgs/msg/string.hpp> // 必要なメッセージヘッダ
json_converter_cpp::Converter converter;
// --- ROS 2 messageからJSONへの変換 ---
std_msgs::msg::String msg;
msg.data = "hello";
rapidjson::Document doc;
doc.SetObject();
auto& allocator = doc.GetAllocator();
// 変換実行
if (converter.to_json(msg, doc, allocator)) {
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
doc.Accept(writer);
std::cout << "Message to JSON: " << buffer.GetString() << std::endl; // 出力: {"data":"hello"}
}
// --- JSONからROS 2 messageへの変換 ---
rapidjson::Document input;
input.Parse(R"({"data": "world"})");
std_msgs::msg::String output;
// 変換実行
if (converter.to_msg(input, output)) {
std::cout << "JSON to Message: " << output.data << std::endl; // 出力: world
}
2. 型が実行時に決まる場合 (SerializedMessage)
トピック受信やrosbagからの読み込みなど、シリアライズされたデータ(rclcpp::SerializedMessage)を、型名を指定して扱う方法です。
#include "json_converter_cpp/converter.hpp"
#include <rclcpp/serialization.hpp>
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <iostream>
json_converter_cpp::Converter converter;
// --- SerializedMessageからJSONへの変換 (実行時の動的型読み込み) ---
rclcpp::SerializedMessage serialized_msg;
// ... (トピックやrosbagなどから serialized_msg を受信/ロードする処理) ...
// ここでは、std_msgs/msg/String::data="test_data" がシリアライズされていると仮定
rapidjson::Document doc;
doc.SetObject();
auto& allocator = doc.GetAllocator();
// 変換実行: 型名とSerializedMessageを指定
if (converter.to_json("std_msgs/msg/String", serialized_msg, doc, allocator)) {
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
doc.Accept(writer);
std::cout << "SerializedMessage to JSON: " << buffer.GetString() << std::endl; // 出力: {"data":"test_data"}
}
// --- JSONからSerializedMessageへの変換 ---
rapidjson::Document input;
input.Parse(R"({"data": "world"})");
rclcpp::SerializedMessage output;
// 変換実行: 型名と出力用SerializedMessageを指定
if (converter.to_msg("std_msgs/msg/String", input, output)) {
// ... (シリアライズされた output をパブリッシュしたり利用したりする処理) ...
std::cout << "JSON to SerializedMessage successful." << std::endl;
}
3. 型が実行時に決まる場合 (Generic Client/Service向け)
rclcpp::GenericClient など、デシリアライズはしたいが、具体的なメッセージ型が実行時に決まる場合に、std::shared_ptr<void> や void* を介してメッセージオブジェクトを扱う方法です。
#include "json_converter_cpp/converter.hpp"
#include <rapidjson/document.h>
#include <iostream>
#include <memory>
json_converter_cpp::Converter converter;
// サービスの型を例として使用: example_interfaces/srv/AddTwoInts
// --- JSONからメッセージオブジェクトへの変換 ---
rapidjson::Document request_doc;
request_doc.Parse(R"({"a": 5, "b": 3})");
std::shared_ptr<void> request_msg;
// 変換実行: 型名とJSONドキュメント、出力用メッセージポインタを指定
if (converter.to_msg("example_interfaces/srv/AddTwoInts::Request", request_doc, request_msg)) {
// request_msgをGenericClientなどで利用
std::cout << "JSON to Service Request Message successful." << std::endl;
}
// --- メッセージオブジェクトからJSONへの変換 ---
// GenericClientから受け取ったレスポンスオブジェクトのポインタを仮定
// (例: example_interfaces/srv/AddTwoInts::Response のデータで sum=8 を含む)
int sum_value = 8;
void* response_ptr = &sum_value; // 簡略化のためintポインタを使用していますが、実際はメッセージオブジェクトのポインタ
rapidjson::Document response_doc;
response_doc.SetObject();
auto& allocator = response_doc.GetAllocator();
// 変換実行: 型名とメッセージポインタ、出力用JSONドキュメントを指定
if (converter.to_json("example_interfaces/srv/AddTwoInts::Response", response_ptr,
response_doc, allocator)) {
// Output: {"sum":8}
// (JSON出力コードを省略)
std::cout << "Message Object to JSON successful." << std::endl;
}
サンプルツールの応用例(Generic Pub/Sub, Service Client)
json_converter のリポジトリでは、ライブラリの利用方法を具体的に示すための汎用的なCLIツールが提供されています。これらは、JSONデータを用いた通信の実際のフローを確認するための応用例となります。
- Generic Publisher/Subscriber: 任意のトピック名とメッセージ型を指定し、JSONファイルから読み込んだデータをROS 2メッセージとしてパブリッシュしたり、受信したメッセージをJSON形式で出力したりするツールとして応用できます。
- Generic Service Client: JSON形式でリクエストデータを準備し、任意のサービスへコールを実行したり、レスポンスをJSONとして受け取ったりするクライアントツールとして応用できます。
使い方の詳細は、リポジトリのREADMEを参照してください。これらのサンプルツールは、ROS 2と外部システム間のデータ連携レイヤーを構築する際の、具体的な実装のヒントとして活用できます。
設計
json_converter_cpp は、ROS 2の typesupport の仕組みを活用して、メッセージとJSONの相互変換を実現しています。
変換の仕組み
内部的には、以下の2つのtypesupportライブラリを組み合わせて実装されています。
1. rosidl_typesupport_introspection_cpp
ROS 2メッセージの型情報(メタデータ)を提供します。これには、フィールド名(例: "x", "y", "z")、各フィールドの型、メモリ上の配置(オフセット)などが含まれます。
この情報を使用することで、C++ Message Object ↔ JSON の直接的な変換を実現しています。
2. rosidl_typesupport_cpp
SerializedMessage ↔ C++ Message Object の変換を行います。
SerializedMessage は、ネットワーク通信やrosbagでの保存に使われる、RMW実装に依存したバイト列(CDR形式など)です。rosidl_typesupport_cppは、実行時に選択されたRMW実装に対応するシリアライザ/デシリアライザを切り替えて提供してくれるので、それをrclcpp::SerializationBaseに渡して、シリアライズ/デシリアライズをおこなっています。
SerializedMessageとJSONの変換フロー
SerializedMessage とJSONを変換する際、json_converter_cpp は、上記2つのtypesupportを連携させて、以下のフローで処理を行います。
SerializedMessage
↓ rosidl_typesupport_cpp でデシリアライズ
C++ Message Object
↓ rosidl_typesupport_introspection_cpp で変換
JSON
技術的には SerializedMessage から直接JSONに変換することも可能ですが、本ライブラリではCDR形式などのRMW実装依存の詳細から分離し、安定した中間形式であるC++ Message Objectを経由する方法を採用しています。
まとめ
本記事では、ROS 2メッセージとJSONの相互変換を目的としたC++ライブラリ json_converter_cpp の概要と基本的な利用方法、そしてその設計思想を紹介しました。ROS 2システムの外部システム連携やデータ永続化の仕組みを構築する際の参考として、ぜひご活用ください。