---
lang: ja
path: cookbook/linux-sdk/ros-integration
labels: クックブック/Linux SDK/ROS連携を始める
metaTitle: ROS連携を始める | Linux SDK｜ クックブック ｜ SkyWay（スカイウェイ）
---

# 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ブラウザから操作コマンドを送信しロボットを操作する

![操縦画面完成](/media/posts/docs/linux-sdk/ros-webui-move.png)

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

![データの流れ](/media/posts/docs/linux-sdk/ros-convert.png)

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

## 環境構築

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

- OS: Ubuntu 24.04
- ROS: ROS 2 Jazzy

### Gazebo のインストール

[Gazeboのドキュメント](https://gazebosim.org/docs/latest/install_ubuntu/)に従ってインストールを行ってください。

### ros_gz_bridge のインストール

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

こちらについても、[Gazeboのドキュメント](https://gazebosim.org/docs/latest/ros_installation/#installing-the-default-gazebo-ros-pairing)に従ってインストールを行ってください。


### skyway_ros_bridge のビルド

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

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

- [Docker のインストール](https://docs.docker.com/engine/install/ubuntu/)
- [Docker Compose plugin のインストール](https://docs.docker.com/compose/install/linux/)

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

```bash
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 のダウンロードとビルドを行います。

```bash
./scripts/download_sdk.sh
colcon build
```

## Gazebo 上でのロボット準備

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

- [Building your own robot](https://gazebosim.org/docs/latest/building_robot/)
- [Moving the robot](https://gazebosim.org/docs/latest/moving_robot/)

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

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

![Gazeboの準備](/media/posts/docs/linux-sdk/ros-gazebo-base.png)

## カメラと被写体の追加

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

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

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

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

```xml
<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">` タグ内に以下の内容を追加してください。

```xml
<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>` タグ内に以下の内容を追加してください。

```xml
<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>` タグ内に以下の内容を追加してください。

```xml
<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を起動します。

```bash
gz sim building_robot.sdf
```

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

![Gazeboの設定変更1](/media/posts/docs/linux-sdk/ros-gazebo-setup.png)

## 操作のための追加設定

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

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

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

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

```xml
<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
<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 ファイルを作成します。

```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とシークレットキーを指定してください。

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

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

```bash
python -m http.server 8080
```

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

![操縦画面起動時](/media/posts/docs/linux-sdk/ros-webui-default.png)

## 実行手順

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

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

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

```bash
# dockerコンテナ内のシェルを起動
docker compose exec ros bash

# コンテナ内で下記を実行し、skyway_ros_bridgeの環境をセットアップ
source ./install/setup.bash
```

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

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

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

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

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

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

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

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

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

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

```bash
# ホスト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` を起動します。

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

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

```bash
# コンテナ内で下記を実行
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 します。

```bash
# コンテナ内で下記を実行
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 サーバを起動し、ブラウザで遠隔操縦ページを開きます。

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

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

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

![操縦画面接続時](/media/posts/docs/linux-sdk/ros-webui-join.png)

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

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

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

```bash
# コンテナ内で下記を実行
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 へ転送します。

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

### 8. 動作確認

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

![操縦画面完成](/media/posts/docs/linux-sdk/ros-webui-move.png)

## まとめ

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

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

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