ROS連携を始める

SkyWay Linux®︎ SDK (以下、Linux SDK ) では、ROS(Robot Operating System)と連携することで、 ROS上で動作するロボットとSkyWayの各SDKの間で音声・映像・データをリアルタイムに相互接続できます。

また、SkyWayでは、Linux SDKを用いたROSノードのリファレンス実装である skyway_ros_bridge を公開しています。

skyway_ros_bridge について

skyway_ros_bridge は、Linux SDK を用いて下記の相互接続を行うROSノードのリファレンス実装です。

  • SkyWay の VideoStream ⇔ ROS の sensor_msgs/Image / sensor_msgs/CompressedImage トピック
  • SkyWay の DataStream ⇔ ROS の std_msgs/String トピック

skyway_ros_bridge を利用すれば、独自の実装を行わずにROSとSkyWayの連携を簡単に始められます。 コードはオープンソースで公開しているので、必要に応じてカスタマイズして利用いただけます。

リポジトリ: https://github.com/skyway/skyway_ros_bridge

チュートリアル

本ページでは、ロボットの遠隔操作を題材に skyway_ros_bridge の具体的な使い方を説明します。

操作対象のロボットは、ROSと連携可能な3DロボットシミュレータGazebo上に配置します。

全体構成

ロボットの遠隔操作として、以下の2つの機能を実現します。

  • Gazebo 上のロボットに搭載したカメラ映像と、俯瞰カメラの映像をWeb ブラウザ上に配信する
  • Webブラウザから操作コマンドを送信しロボットを操作する

操縦画面完成

映像データ、操作データは下記のような流れで転送します。

データの流れ

ROS と SkyWay 間のデータは skyway_ros_bridge を利用して転送し、Gazebo と ROS 間のデータは ros_gz_bridge パッケージを利用して転送します。

環境構築

想定環境は以下の通りです。

  • OS: Ubuntu 24.04
  • ROS: ROS 2 Jazzy

Gazebo のインストール

Gazeboのドキュメントに従ってインストールを行ってください。

ros_gz_bridge のインストール

Gazebo と ROS 間でトピックを転送するために ros_gz_bridge パッケージをインストールします。

こちらについても、Gazeboのドキュメントに従ってインストールを行ってください。

skyway_ros_bridge のビルド

skyway_ros_bridge は Docker 環境上で簡単にビルド、実行することができます。

Docker / Docker Compose plugin がインストールされていない場合は、公式ドキュメントに従ってインストールを行ってください。

Dockerのインストールが完了したら、リポジトリをクローンし、Docker環境を立ち上げます。

git clone --recursive https://github.com/skyway/skyway_ros_bridge cd skyway_ros_bridge docker compose up -d docker compose exec ros bash

Dockerコンテナ内のシェル上で、下記コマンドを実行し Linux SDK のダウンロードとビルドを行います。

./scripts/download_sdk.sh colcon build

Gazebo 上でのロボット準備

本ページでは、下記のGazebo 公式チュートリアルに従って作成したロボットをベースとします。まずは、下記のチュートリアルを順番に実施し、ロボットモデルを作成してください。

実施後、下記の状態になっていることを確認してください。

  • ロボットモデル(vehicle_blue)が Gazebo 上に配置されている
  • キー入力によりロボットを操作できる

Gazeboの準備

カメラと被写体の追加

SkyWayで映像を確認しながら操作できるように、以下の要素をGazebo上に追加します。

  • ロボットに搭載するカメラ
  • 俯瞰視点の定点カメラ
  • 被写体となるオブジェクト

センサープラグインの追加

カメラセンサーを動作させるため、SDFファイル building_robot.sdf にプラグインを追加します。ファイル内の <world> タグ内に以下のプラグイン設定を追加してください。

<plugin filename="gz-sim-sensors-system" name="gz::sim::systems::Sensors"> <render_engine>ogre</render_engine> </plugin>

ロボット搭載カメラの追加

次にロボットモデル vehicle_blue の中に、ロボットの chassis に固定されたカメラリンク camera_link を追加し、camera センサーを載せます。 <model name="vehicle_blue"> タグ内に以下の内容を追加してください。

<link name="camera_link"> <pose relative_to="chassis">1 0 0.3 0 0 0</pose> <inertial> <mass>0.1</mass> <inertia> <ixx>0.0001</ixx> <iyy>0.0001</iyy> <izz>0.0001</izz> <ixy>0</ixy> <ixz>0</ixz> <iyz>0</iyz> </inertia> </inertial> <visual name="camera_visual"> <geometry> <box> <size>0.05 0.05 0.2</size> </box> </geometry> <material> <ambient>0.0 0.0 0.0 1</ambient> <diffuse>0.1 0.1 0.1 1</diffuse> <specular>0.1 0.1 0.1 1</specular> </material> </visual> <sensor name="camera" type="camera"> <pose>0 0 0 0 0 0</pose> <always_on>true</always_on> <update_rate>30</update_rate> <visualize>true</visualize> <camera> <horizontal_fov>1.3962634</horizontal_fov> <image> <width>640</width> <height>480</height> <format>R8G8B8</format> </image> <clip> <near>0.1</near> <far>100</far> </clip> </camera> <topic>/camera/image</topic> </sensor> </link> <joint name="camera_joint" type="fixed"> <parent>chassis</parent> <child>camera_link</child> </joint>

これにより、指定したトピック名 /camera/image でロボットに搭載したカメラ画像が配信されるようになります。

定点カメラの追加

俯瞰して状況を確認できるよう、定点カメラ fixed_camera を追加します。画像の Gazebo トピック名は /fixed_camera/image とします。

<world> タグ内に以下の内容を追加してください。

<model name="fixed_camera"> <static>true</static> <pose>-5 0 10 0 1 0</pose> <link name="camera_link"> <sensor name="fixed_camera_sensor" type="camera"> <pose>0 0 0 0 0 0</pose> <always_on>true</always_on> <update_rate>30</update_rate> <visualize>true</visualize> <camera> <horizontal_fov>1.3962634</horizontal_fov> <image> <width>640</width> <height>480</height> <format>R8G8B8</format> </image> <clip> <near>0.1</near> <far>100</far> </clip> </camera> <topic>/fixed_camera/image</topic> </sensor> </link> </model>

被写体の追加

カメラ前方に被写体として test_box という名前で立方体を配置します。こちらも <world> タグ内に以下の内容を追加してください。

<model name="test_box"> <static>true</static> <pose>5 0 1 0 0 0</pose> <link name="link"> <inertial> <mass>1.0</mass> <inertia> <ixx>1</ixx><iyy>1</iyy><izz>1</izz> <ixy>0</ixy><ixz>0</ixz><iyz>0</iyz> </inertia> </inertial> <collision name="collision"> <geometry> <box> <size>2 2 2</size> </box> </geometry> </collision> <visual name="visual"> <geometry> <box> <size>2 2 2</size> </box> </geometry> <material> <ambient>0.1 0.8 0.1 1</ambient> <diffuse>0.1 0.8 0.1 1</diffuse> <specular>0.1 0.1 0.1 1</specular> </material> </visual> </link> </model>

ここまでの変更を保存し、Gazeboを起動します。

gz sim building_robot.sdf

下記のような状態になっていることを確認してください。

Gazeboの設定変更1

操作のための追加設定

Gazeboのチュートリアルでは /cmd_vel トピックに Twist メッセージを送信することでロボットを操作しています。

SkyWay経由で送信される操作コマンドは文字列であるため、これをTwistメッセージに変換する設定を行います。具体的には、SkyWayから /cmd_vel_string トピックにデータを送信し、TriggeredPublisher プラグインを用いて、対応する Twist メッセージに変換して /cmd_vel に送信します。

操作は、前進(forward)/後退(backward)/左旋回(left)/右旋回(right)/停止(stop)の5種類を定義します。

<world> タグ内に以下の内容を追加してください。

<plugin filename="gz-sim-triggered-publisher-system" name="gz::sim::systems::TriggeredPublisher"> <input type="gz.msgs.StringMsg" topic="/cmd_vel_string"> <match field="data">"forward"</match> </input> <output type="gz.msgs.Twist" topic="/cmd_vel"> linear: {x: 0.5} angular: {z: 0.0} </output> </plugin> <plugin filename="gz-sim-triggered-publisher-system" name="gz::sim::systems::TriggeredPublisher"> <input type="gz.msgs.StringMsg" topic="/cmd_vel_string"> <match field="data">"backward"</match> </input> <output type="gz.msgs.Twist" topic="/cmd_vel"> linear: {x: -0.5} angular: {z: 0.0} </output> </plugin> <plugin filename="gz-sim-triggered-publisher-system" name="gz::sim::systems::TriggeredPublisher"> <input type="gz.msgs.StringMsg" topic="/cmd_vel_string"> <match field="data">"left"</match> </input> <output type="gz.msgs.Twist" topic="/cmd_vel"> linear: {x: 0.0} angular: {z: 0.5} </output> </plugin> <plugin filename="gz-sim-triggered-publisher-system" name="gz::sim::systems::TriggeredPublisher"> <input type="gz.msgs.StringMsg" topic="/cmd_vel_string"> <match field="data">"right"</match> </input> <output type="gz.msgs.Twist" topic="/cmd_vel"> linear: {x: 0.0} angular: {z: -0.5} </output> </plugin> <plugin filename="gz-sim-triggered-publisher-system" name="gz::sim::systems::TriggeredPublisher"> <input type="gz.msgs.StringMsg" topic="/cmd_vel_string"> <match field="data">"stop"</match> </input> <output type="gz.msgs.Twist" topic="/cmd_vel"> linear: {x: 0.0} angular: {z: 0.0} </output> </plugin>

これで /cmd_vel_string トピックに forward などの文字列を送信することで、ロボットを操作できるようになります。

以上でSDFファイルの編集は完了です。

遠隔操縦画面を作成

Gazebo上でのカメラ等の準備が完了したので、次に SkyWay JavaScript SDK を利用して遠隔操縦画面を作成します。

遠隔操縦画面では、ロボットからの映像を受信して表示し、ボタン操作により操作コマンドを DataStream として送信する機能を実装します。

まずは任意のディレクトリを作り、その中に index.html という名前で以下の HTML ファイルを作成します。

<html> <body> <div> room name: <input id="room-name" type="text" /> <button id="join">join</button> <button id="leave">leave</button> </div> <div>DataStream PublicationId: <span id="data-publication-id"></span></div> <div> controller: <button class="controller-button" data-type="forward">forward</button> <button class="controller-button" data-type="backward">backward</button> <button class="controller-button" data-type="left">left</button> <button class="controller-button" data-type="right">right</button> <button class="controller-button" data-type="stop">stop</button> </div> <div id="videos"></div> <script src="https://cdn.jsdelivr.net/npm/@skyway-sdk/room/dist/skyway_room-latest.js"></script> <script src="main.js"></script> </body> </html>

次に、main.js という名前で以下の JavaScript ファイルを作成します。

const { nowInSec, SkyWayAuthToken, SkyWayContext, SkyWayRoom, SkyWayStreamFactory, uuidV4, } = skyway_room const appId = "<AppId>" // SkyWay コンソールから取得したアプリケーションIDを指定してください const secret = "<SecretKey>" // SkyWay コンソールから取得したシークレットキーを指定してください const token = new SkyWayAuthToken({ jti: uuidV4(), iat: nowInSec(), exp: nowInSec() + 60 * 60 * 24, version: 3, scope: { appId: appId, rooms: [ { name: "*", methods: ["create", "close", "updateMetadata"], member: { name: "*", methods: ["publish", "subscribe", "updateMetadata"], }, }, ], turn: { enabled: true, }, }, }).encode(secret) void (async () => { const roomNameInput = document.getElementById("room-name") const joinButton = document.getElementById("join") const leaveButton = document.getElementById("leave") const videosArea = document.getElementById("videos") const dataPublicationIdSpan = document.getElementById("data-publication-id") const controllerButtons = document.querySelectorAll(".controller-button") const dataStream = await SkyWayStreamFactory.createDataStream() controllerButtons.forEach((button) => { button.onclick = () => { const command = button.getAttribute("data-type") dataStream.write(command) } }) joinButton.onclick = async () => { if (roomNameInput.value === "") return const context = await SkyWayContext.Create(token) const room = await SkyWayRoom.FindOrCreate(context, { name: roomNameInput.value, }) const me = await room.join() const dataPublication = await me.publish(dataStream, { type: "p2p" }) dataPublicationIdSpan.textContent = dataPublication.id const subscribeAndAttach = async (publication) => { if (publication.contentType !== "video") return const { stream } = await me.subscribe(publication.id) const newMedia = document.createElement("video") newMedia.playsInline = true newMedia.autoplay = true newMedia.id = `media-${publication.id}` stream.attach(newMedia) videosArea.appendChild(newMedia) } room.publications.forEach(subscribeAndAttach) room.onStreamPublished.add((e) => subscribeAndAttach(e.publication)) leaveButton.onclick = async () => { await me.leave() await room.dispose() videosArea.replaceChildren() } room.onStreamUnpublished.add((e) => { document.getElementById(`media-${e.publication.id}`)?.remove() }) } })()

ファイル内の <AppId><SecretKey> には、SkyWay コンソールから取得したアプリケーションIDとシークレットキーを指定してください。

const appId = "<AppId>" // SkyWay コンソールから取得したアプリケーションIDを指定してください const secret = "<SecretKey>" // SkyWay コンソールから取得したシークレットキーを指定してください

ファイルの作成が完了したら、簡易的な Web サーバを起動します。

python -m http.server 8080

ブラウザで http://localhost:8080 にアクセスすると、下記のような画面が表示されます。

操縦画面起動時

実行手順

準備が整ったので、実際に動作させてみましょう。

以下、各種コマンドはGazeboを動かしているホストOS側のシェルまたはDockerコンテナ内のシェル上で実行する形になります。

Dockerコンテナ内のシェルで実行する際は、下記のコマンドを実施した上で実行してください。

# dockerコンテナ内のシェルを起動 docker compose exec ros bash # コンテナ内で下記を実行し、skyway_ros_bridgeの環境をセットアップ source ./install/setup.bash

1. Gazebo でシミュレーションを起動する

SDF を指定して Gazebo を起動します。

# ホストOSで下記を実行 gz sim building_robot.sdf

起動後、GUI 上で Play ボタンを押してシミュレーションを開始してください。

2. Gazebo のカメラ画像を ROS に転送する

Gazebo 側で発行される /camera/image/fixed_camera/image を ROSへ転送します。

# ホストOSで下記を実行 ros2 run ros_gz_image image_bridge /camera/image

別のターミナルで下記も実行してください。

# ホストOSで下記を実行 ros2 run ros_gz_image image_bridge /fixed_camera/image

ROS 上に Image トピックが存在していることを確認します。

# ホストOSで下記を実行 ros2 topic list

実行結果として下記のように /camera/image/fixed_camera/image トピックが表示されていれば問題ありません。

/camera/image /camera/image/compressed /camera/image/compressedDepth /camera/image/theora /camera/image/zstd /fixed_camera/image /fixed_camera/image/compressed /fixed_camera/image/compressedDepth /fixed_camera/image/theora /fixed_camera/image/zstd

3. skyway_ros_bridge を起動し、SkyWayのRoomに参加する

Dockerコンテナ内のシェル上で skyway_ros_bridge を起動します。

# コンテナ内で下記を実行 ros2 run skyway_ros_bridge skyway

別ターミナルでもDockerコンテナ内のシェルで、下記コマンドを実行し、SkyWay の Room に参加します。この時 <AppId><SecretKey> は SkyWay のコンソールから取得したアプリケーションIDとシークレットキーを指定してください。

# コンテナ内で下記を実行 ros2 service call /join_room skyway_ros_bridge_msgs/srv/JoinRoom "{skyway_app_id: '<AppId>', skyway_secret_key: '<SecretKey>', room_name: 'ros_tutorial'}"

4. ROS の画像トピックを SkyWay に転送する

ROS 上のカメラ映像を SkyWay の VideoStream として Publish します。

# コンテナ内で下記を実行 ros2 service call /publish_video_stream_to_skyway_by_image skyway_ros_bridge_msgs/srv/PublishVideoStreamToSkywayByImage "{topic_name: '/camera/image'}" ros2 service call /publish_video_stream_to_skyway_by_image skyway_ros_bridge_msgs/srv/PublishVideoStreamToSkywayByImage "{topic_name: '/fixed_camera/image'}"

5. 遠隔操縦ページを開く

Web サーバを起動し、ブラウザで遠隔操縦ページを開きます。

# ホストOSで下記を実行 python -m http.server 8080 open http://localhost:8080

ページ上部のルーム名に ros_tutorial join ボタンを押して Room に参加します。

この時点で、Gazebo 上のロボットカメラ映像がブラウザ上に表示されることを確認してください。

操縦画面接続時

6. SkyWay の DataStream を ROS に転送する

遠隔操縦ページで操作コマンドを送信できるよう、SkyWay の DataStream を ROS の /cmd_vel_string トピックへ転送する設定を行います。

遠隔操縦ページに表示されている DataStream PublicationId を下記コマンドの <PublicationId> 部分に指定し、SkyWay → ROS の転送を開始します。

# コンテナ内で下記を実行 ros2 service call /publish_data_topic_from_skyway skyway_ros_bridge_msgs/srv/PublishDataTopicFromSkyway "{publication_id: '<PublicationId>', topic_name: '/cmd_vel_string'}"

7. ROS の トピックを Gazebo へ転送する

ここまでの操作で、SkyWay 経由で送信された操作コマンドが ROS の /cmd_vel_string トピックに到達するようになりました。

次にROSの /cmd_vel_string トピックを Gazebo へ転送します。

# ホストOSで下記を実行 ros2 run ros_gz_bridge parameter_bridge /cmd_vel_string@std_msgs/msg/String@gz.msgs.StringMsg

8. 動作確認

遠隔操作ページで操作ボタンを押して、ロボットが動くことを確認してください。

操縦画面完成

まとめ

本ページでは、skyway_ros_bridgeを用いてSkyWayとROSを連携させる方法紹介しました。

今回紹介した機能以外にも、skyway_ros_bridgeではSkyWay側の映像をROSに転送したり、ROSからRTP映像入力を制御することも可能です。 今回紹介した機能以外にも、skyway_ros_bridgeではSkyWay側の映像をROSに転送する機能や、ROSからRTP映像入力を制御する機能を提供しています。 詳しくは、skyway_ros_bridgeのREADME.mdをご覧ください。

商標

Linux®︎は、米国およびその他の国における Linus Torvalds の登録商標です。