ROS講座47 GamePadをブラウザで使う

# 環境
この記事は以下の環境で動いています。

| 項目 | 値 |
|:-:|:-:|
| CPU | Core i5-8250U |
| Ubuntu | 16.04 |
| ROS | Kinetic |

インストールについては[ROS講座02 インストール](https://qiita.com/srs/items/e0e0a9dc3f94c2d3348e)を参照してください。
またこの記事のプログラムはgithubにアップロードされています。[ROS講座11 gitリポジトリ](https://qiita.com/srs/items/25a56a6abda41dcf396e)を参照してください。

# 概要
ロボットをリモートから監視、操作するときにWeb経由はうってつけの物です。Webの技術は出力（表示）系は充実していますが、入力系は十分とは言えません。そこで`GamepadAPI`を使ってGamePadの入力値をWeb経由でROSに送ります。

# GamePad APIのサンプル

## ソースコード

Gamepad APIを使うhtmlのサンプルです。

```html:web_lecture/www/gamepad_api_test.html
<!doctype html>
<html lang="ja">
  <head>
    <title>Gamepad DEmo</title>
    <meta charset="UTF-8">
  </head>
    <body>
      <div id="disp">test</div>
      <script>

//Create AnimationFrame
var rAF = window.mozRequestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.requestAnimationFrame; 

//Update
function update() {
  var pads = navigator.getGamepads ? navigator.getGamepads() :
    (navigator.webkitGetGamepads ? navigator.webkitGetGamepads : []);

  //tekitou
  pads = pads[0];
	if(pads) {
		//buttons
		var but = [];
		for(var i = 0 ; i < pads.buttons.length; i++) {
			var val = pads.buttons[i];
			var pressed = val == 1.0;
			if (typeof(val) == "object") {
				pressed = val.pressed;
				val     = val.value;
			}
			but[i] = val;
		}

		var txt = [];
		txt += pads.id + "<br>";
		for(var i = 0 ; i < but.length; i++) {
			if( but[i] == 1)  txt += '<input type="checkbox" checked="checked">';
			else              txt += '<input type="checkbox" >';
		}

		var axes = pads.axes;
		for(var i = 0 ; i < axes.length; i++) {
			txt += '<br>';
			txt += axes[i] ;
		}
		console.log(axes);
		document.getElementById("disp").innerHTML = txt;
	}
}

//Start
setInterval("update()",100);

    </script>
  </body>
</html>
```

## 実行

GamePadをPCに挿してこのページを開くと以下のように見えます。ボタンを押したかや軸の値が見えます。

![web_gamepad_api.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254442/713518fd-e905-636f-df76-434084cfe43b.png)

# Web経由でGamePadをROSに接続する

## ソースコード

GamePad APIで取得したGamePadのデータをWebSocketを使ってROSに流します。

```html:web_lecture/www/gamepad.html
<!doctype html>
<html lang="ja">
  <head>
    <title>ROS UI</title>
    <meta charset="UTF-8">
    <style>
    *{
      margin: 0px;
      padding: 0px;
      border: 0px;
    }
    .full_picture{
      width: 100vw;
      height: 100vh;
      object-fit: contain;
    }
    </style>
    <script src="https://static.robotwebtools.org/EventEmitter2/current/eventemitter2.min.js"></script>
    <script src="https://static.robotwebtools.org/roslibjs/current/roslib.min.js"></script>
  </head>
  
  <body>
    <script>
      var topic_name = "/web/image_raw";
      document.write("<img class='full_picture' src='http://"+location.hostname +":8080/stream?topic=" + topic_name + "&type=ros_compressed'></img>");
    </script>
    <p>RosBridge status： <label id="rosbridge_status">Disconnect</label></p>
    <p>Gamepad status：   <label id="gamepad_status">Disconnect</label></p>
    <p>Gamepad buttons：   <div id="gamepad_buttons">None</div></p>
    <p>Gamepad axes：   <div id="gamepad_axes">None</div></p>
  </body>

  <script>
var Talker = {
  ros : null,
  name : "",
  init : function(){
    this.ros = new ROSLIB.Ros();
    this.ros.on('error', function(error) {
        document.getElementById('rosbridge_status').innerHTML = "Error";
    });
    this.ros.on('connection', function(error) {
        document.getElementById('rosbridge_status').innerHTML = "Connect";
    });
    this.ros.on('close', function(error) {
        document.getElementById('rosbridge_status').innerHTML = "Close";
    });
    this.ros.connect('ws://' + location.hostname + ':9090');
  },
  send : function(joy_data){
    if(joy_data != null){
      var pub = new ROSLIB.Topic({
          ros : this.ros,
          name : '/web/joy',
          messageType : 'sensor_msgs/Joy'
      });
      pub.publish(joy_data);
    }
  },
  getJoy : function() {
    var pads = navigator.getGamepads ? navigator.getGamepads() :
                    (navigator.webkitGetGamepads ? navigator.webkitGetGamepads : []);
    
    var joy_data = new ROSLIB.Message({  axes:[], buttons:[]});
    pad = pads[0];
    if(pad) {
      document.getElementById("gamepad_status").innerHTML = "Connected";

      //buttons for display
      var but = "";
      for(var i = 0 ; i < pad.buttons.length; i++) {
        but += "but" + i + ":" + pad.buttons[i].value + "<br>";//or .pressed
      }
      document.getElementById("gamepad_buttons").innerHTML = but;
      
      //axes for display
      var ax = "";
      for(var i = 0 ; i < pad.axes.length; i++) {
        ax += pad.axes[i] + '<br>';
      }
      document.getElementById("gamepad_axes").innerHTML = ax;

      //for json
      for(var i = 0 ; i < pad.buttons.length; i++) {
        joy_data.buttons.push(Number(pad.buttons[i].pressed));      
      }
      for(var i = 0 ; i < pad.axes.length; i++) {
        joy_data.axes.push(pad.axes[i]);
      }    
      for(var i = 0 ; i < pad.buttons.length; i++) {
        joy_data.axes.push(pad.buttons[i].value);      
      }
      console.log(joy_data);
      return joy_data
    }
    else{
      document.getElementById("gamepad_status").innerHTML = "Disonnected";
      joy_data = null
    }
  }
}
Talker.init();

window.onload = function(){
};
window.onunload = function(){
  Talker.ros.close();
};

function update(){
  joy_data = Talker.getJoy();
  Talker.send(joy_data);
}
setInterval("update()",100);
  </script>
</html>
```

gazeboでcameraとtwistでの移動機構をspawnします。

```xml:web_lecture/launch/web_if.launch
<launch>
  <arg name="model" default="$(find web_lecture)/xacro/odm_simple.xacro" />
  <param name="robot_description" command="$(find xacro)/xacro.py $(arg model)" />

  <include file="$(find roswww)/launch/roswww.launch" />
  <node pkg="web_video_server" type="web_video_server" name="web_video_server" />
  <include file="$(find rosbridge_server)/launch/rosbridge_websocket.launch" />

  <include file="$(find gazebo_ros)/launch/empty_world.launch">
    <arg name="paused" value="false"/>
    <arg name="use_sim_time" value="true"/>
    <arg name="gui" value="true"/>
    <arg name="headless" value="false"/>
    <arg name="debug" value="false"/>
  </include>

  <node name="spawn_urdf" pkg="gazebo_ros" type="spawn_model" args="-param robot_description -urdf -model odm_robot" />

  <node name="image_republish" pkg="image_transport" type="republish" args="compressed compressed">
    <remap from="in" to="/head_camera/image_raw" />
    <remap from="out" to="/web/image_raw" />
  </node>
  <node name="twist_publisher" pkg="web_lecture" type="twist_publisher.py" />
</launch>
```

## 実行
```shell
roslaunch web_lecture web_if.launch 
```

GamePadをPCに接続した後にブラウザで`http://localhost:8085/web_lecture/gamepad.html`を開きます。GamePadを操作することでシミュレーター中のロボットが動きます。

![web_gamepad.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254442/db48ec50-43d0-3f0e-91cd-c21643ee266b.png)

# 実行

# スマホから使う
今回のWeb系統の技術はROSの動作しているPCからの通信ではなく、他の端末からの通信で真価を発揮します。今回はスマホにGamePadを使って上記の通信をやってみます。

![iphone_connect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254442/dcb1734f-ed33-406d-7857-5267b355ac5c.png)

iphoneではiOS12以降のsafariでGamepadAPIが対応しているようです。またiphoneではMFIという認証を取っているGamepadしか使えないようです。以下の構成で今回のプログラムが動作することを確認しました。手元で試したところGamepadAPIでの接続が多少不安定な感じがあります。Gamepadを開く->safariで上記のページを開く->画面を何回かタッチする。といった作業が必要です。たまにつながらないときがありますが、その時はiphoneの再起動をするとつながるようになります。

| 項目 | 値 |
|:-:|:-:|
| 端末 | iphone6s |
| OS | ios12 |
| ブラウザ | safari |
| GamePad | PXN-6603 |

![iPhoneJoy.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254442/e0d8fbef-f83b-ebbd-18f3-1a8a4e7dc839.jpeg)

# 参考
[GamePad APIについて](https://qiita.com/gyabo/items/baae116e9e4c53ca17ab)

# 目次ページへのリンク
[ROS講座の目次へのリンク](https://qiita.com/srs/items/5f44440afea0eb616b4a)