Webブラウザからドローンにコマンドを送る

#はじめに
このページは，

[ドローン操作システムを作ろう](https://qiita.com/hsgucci/items/86eedb5555b4234ee0e7)

の１ページです．
全体を見たい場合は上記ページへお戻りください．

#概要

前回は，PythonプログラムからMQTTでドローンを操作するコマンドを送ってみました．
しかし，WebブラウザとPythonのGUIプログラムを並べて使うというのは，イマイチ格好わるいです．
やはりWebブラウザからフルオペレーションできるほうが **<font color="blue">Cool！</font>** ですね．
今回はそれをやります．

#システム構成

![browser_operation.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256470/ada5a929-ea5a-387a-d58f-f7757c9a69c5.png)

ドローンのシミュレータは前回使った　「[gui_sitl_pubsub.py](https://raw.githubusercontent.com/DCoJA/OpenUVTM/master/chapter13/gui_sitl_pubsub.py)」　です．

Webブラウザ用のプログラムを　「map_button.html」　として新しく作ります．

#Webブラウザ用のプログラム

[ここ](https://raw.githubusercontent.com/DCoJA/OpenUVTM/master/chapter14/map_button.html)　を右クリック-[名前を付けて保存]するか，
以下のコードをコピー＆ペーストしてファイルを作成してください．

```html:map_button.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8" /><!-- 文字コードはutf-8を使用する -->
	<title>Button Control</title><!-- タイトルバーに表示されるメッセージ -->

	<!-- 以下の2行でLeafletで使用するスタイルシート(.css)とライブラリ(.js)を読み込む -->
	<link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""/>
	<script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js" integrity="sha512-GffPMF3RvMeYyc1LWMHtK8EbPv0iNZ8/oTtHPx9/cc2ILxQ+u905qIwdpULaqDkyBKgOaB57QTMg7ztg8Jm2Og==" crossorigin=""></script>

	<!-- マーカーの移動・回転ができるプラグインを読み込む -->
	<script src="MovingMarker.js" type="text/javascript"></script>
	<script src="leaflet.rotatedMarker.js" type="text/javascript"></script>


	<!-- 以下の2行で，MQTT over Websocketを使うライブラリ(.js)を読み込む -->
	<script src="//ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
	<script src="mqttws31.js" type="text/javascript"></script>

	<!-- ログ表示用のスタイルシートなので，これは消さない -->
	<style>
	.box {
		width: 440px;
		float: left;
		margin: 0 20px 0 20px;
	}

	.box div, .box input {
		border: 1px solid;
		-moz-border-radius: 4px;
		border-radius: 4px;
		width: 100%;
		padding: 5px;
		margin: 3px 0 10px 0;
	}

	.box div {
		border-color: grey;
		height: 300px;
		overflow: auto;
	}

	div code {
		display: block;
	}

	#second div {
		font-size: 0.6em;
	}
	</style>
</head>

<body>
<!-- このmapidと名付けられたdiv要素の中に地図を表示する -->
<div id="mapid" style="width: 950px; height: 400px;"></div>

<!-- エディットボックスを作る部分．#firstの方は削除した -->
<div id="second" class="box">
	<h2>Logs</h2>
	<div></div>
</div>

<!-- ドローンを操作するボタンを配置 -->
<div>
	<h2>Control</h2>
	<div>
		<form><!-- onclickイベントでOnButtonClick関数を呼んで，ドローンにコマンドをsendする -->
		<input type="button" value="GUIDED" onclick="OnButtonClick('GUIDED');" ><input type="button" value="RTL" onclick="OnButtonClick('RTL');" ><br>
		<input type="button" value="ARM" onclick="OnButtonClick('ARM');" ><input type="button" value="DISARM" onclick="OnButtonClick('DISARM');" ><br>
		<input type="button" value="TAKEOFF" onclick="OnButtonClick('TAKEOFF');" ><input type="button" value="LAND" onclick="OnButtonClick('LAND');" ><br>

		<input type="button" value="GOTO" onclick="OnButtonClick('GOTO');">
		<input class='form-control' type="text" size="10" name="lat" id="lat" placeholder="Latitude" value="35.893246" />
		<input class='form-control' type="text" size="10" name="lon" id="lon" placeholder="Longitude" value="139.954909" />
		<input class='form-control' type="text" size="3" name="alt" id="alt" placeholder="Altitude" value="30" />
		</form>
	</div>
</div>

<!-- <script></script>で囲まれた部分がJavaScript -->
<script>
	// mapidと名の付いたdiv要素に地図を作成し，視点は柏の葉キャンパス駅前付近，ズームレベルは16に設定
	var mymap = L.map('mapid').setView([35.894087,139.952447], 17);

	// OpenStreetMapのタイルレイヤーを作る
	var tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
		attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>',
		maxZoom: 19
	});
	tileLayer.addTo(mymap); // 作成したtileLayerをmymapに追加する

	// マーカーにする画像を読み込む
	var quad_x_Icon		= L.icon({ iconUrl: 'quad_x-90.png', iconRetinaUrl: 'quad_x-90.png', iconSize: [50, 50], iconAnchor: [25, 25], popupAnchor: [0, -50] });

	// Log表示用の関数部分
	var has_had_focus = false;
	var pipe = function(el_name, send) {
		var div  = $(el_name + ' div');
		var inp  = $(el_name + ' input');
		var form = $(el_name + ' form');

		var print = function(m, p) {
			p = (p === undefined) ? '' : JSON.stringify(p);
			div.append($("<code>").text(m + ' ' + p));
			div.scrollTop(div.scrollTop() + 10000);
		};
		return print;
	};
	var debug = pipe('#second');

	// MQTT over WebSocketの初期化
	var wsbroker = location.hostname;	// MQTTブローカーは自分自身
	var wsport = 15675;	// MQTTの標準ポート番号は1883だが，WebSocketは15675とした(RabbitMQと同じ仕様)

	// MQTTのクライアントを作成する クライアントID名はランダムに作る
	var client = new Paho.MQTT.Client(wsbroker, wsport, "/ws", "myclientid_" + parseInt(Math.random() * 100, 10));

	//== ここからボタンを押した時の処理=============================================
	// ドローン操作用のコマンド
	var command = {
				"command":"None",
				"d_lat":"0",
				"d_lon":"0",
				"d_alt":"0",
	}
	
	// HTML上のボタンが押された時の処理
	function OnButtonClick(str) {
		command["command"] = str;	// 引数の文字列がそのままコマンドになる

		// GOTOのときは，緯度/経度/高度も取得してコマンドを作る
		if( str == "GOTO" ) {
			lat = document.getElementById("lat").value;
			lon = document.getElementById("lon").value;
			alt = document.getElementById("alt").value;
			command["d_lat"] = lat;
			command["d_lon"] = lon;
			command["d_alt"] = alt;
		}

		// JSON型にしてからMQTTでPublishする
		json_command = JSON.stringify(command);		// JSON型にする
		message = new Paho.MQTT.Message(json_command);		// MQTTのメッセージパケットを作る
		message.destinationName = "ctrl/001";	// トピック名を設定
		client.send(message);	// MQTTでPubする
		debug("SEND ON " + message.destinationName + " PAYLOAD " + message.payloadString);   //debugボックスに表示
	}
	//=======================ボタンを押した時の処理　ここまで=============

	// 切断時のコールバック
	client.onConnectionLost = function (responseObject) {
		debug("CONNECTION LOST - " + responseObject.errorMessage);
	};

	// ドローン情報保存用の連想配列
	var drones = new Array();   // 緯度経度などのデータを保存する連想配列
	var markers = new Array();  // マーカーハンドラを保存する連想配列

	// MQTTメッセージSubscribe時のコールバック
	client.onMessageArrived = function (message) {
		debug("RECEIVE ON " + message.destinationName + " PAYLOAD " + message.payloadString);   //debugボックスに表示

		var drone_name = message.destinationName;   // ドローン名はトピック名とする

		var drone_data = JSON.parse( message.payloadString );   // ドローンのデータを連想配列にして格納

		var mode = drone_data.status.Arm;           // ARM/DISARM
		var arm  = drone_data.status.FlightMode;    // フライトモード
		var lat  = parseFloat( drone_data["position"]["latitude"] );  // 緯度
		var lon  = parseFloat( drone_data["position"]["longitude"] ); // 経度
		var alt  = parseFloat( drone_data["position"]["altitude"] );  // 高度
		var ang  = parseFloat( drone_data["position"]["heading"] );   // 方位

		// ポップアップ用に文字列を作る +=で追加していく
		var drone_popmessage = drone_name + '<br>';
		drone_popmessage += mode + ',' + arm + '<br>';
		drone_popmessage += lon + ',' + lat + '<br>';
		drone_popmessage += alt + '[m], ' + ang + '[deg]<br>';

		// drone_nameで登録されている連想配列があるか？ ない->新規にマーカー作成 ある->なにもしない
		if( !drones[drone_name] ) {

			// movingMarkerでマーカーを作成し，rotatedMarkerのrotationオプションをつける
			markerHandle = L.Marker.movingMarker([[lat, lon]], [], 
							{	rotationAngle: 0,   // 回転角度
								rotationOrigin: 'center center',    // 回転中心
								title:drone_name,
								contextmenu: true,
								contextmenuItems: [{
									text: drone_name,
									index: 0
								}, {
									separator: true,
									index: 1
								}]
							});
			markerHandle.bindPopup( drone_popmessage ); // ポップアップメッセージの設定
			markerHandle.options.icon = quad_x_Icon;    // マーカーアイコンをオリジナル画像に
			markerHandle.options.autostart = true;	    // MovingMarker機能を自動スタート
			markerHandle.addTo( mymap );                // 地図へ追加

			markers[drone_name] = markerHandle;	// マーカー用連想配列に今回作ったマーカー情報を保存
		} else {
			markerHandle = markers[drone_name]  // 保存されているマーカー情報をdrone_name(トピック名)をキーにして呼び出す
			markerHandle.moveTo([lat,lon], 1000);  // MovingMarkerの移動機能
			markerHandle.setRotationAngle(ang);	   // ratatedMarkerの回転機能
			markerHandle.setPopupContent( drone_popmessage );	// メッセージ更新
		}
		drones[drone_name] = drone_data; // ドローンデータ用連想配列の情報を最新のメッセージに更新

	};

	// MQTTの接続オプション
	var options = {
		timeout: 3,
		onSuccess: function () {
			debug("CONNECTION SUCCESS");
			client.subscribe('drone/#', {qos: 1});
		},
		onFailure: function (message) {
			debug("CONNECTION FAILURE - " + message.errorMessage);
		}
	};

	// サーバーがHTTPS対応だった時の処理
	if (location.protocol == "https:") {
		options.useSSL = true;
	}

	// 最初にメッセージを表示してMQTTをブローカーに接続
	debug("CONNECT TO " + wsbroker + ":" + wsport);
	client.connect(options);		// 接続
</script>

</body>
</html>
```

保存したmap_button.htmlを，Webサーバーが利用するフォルダへコピーします．
管理者権限が必要なのでsudoを付けます．

```bash:apacheのフォルダへコピー
$ sudo cp map_button.html /var/www/html/
```
#プログラム解説

前々回の　[marker_moving_rotated.html](https://github.com/DCoJA/OpenUVTM/blob/master/chapter12/marker_moving_rotated.html)　から大きく変化した部分だけ解説します．

##body部

前回のGUIプログラムの様に，ボタンをクリックするとコマンドを送信するようにします．

```html:inputフォームを配置
<!-- ドローンを操作するボタンを配置 -->
<div>
	<h2>Control</h2>
	<div>
		<form><!-- onclickイベントでOnButtonClick関数を呼んで，ドローンにコマンドをsendする -->
		<input type="button" value="GUIDED" onclick="OnButtonClick('GUIDED');" ><input type="button" value="RTL" onclick="OnButtonClick('RTL');" ><br>
		<input type="button" value="ARM" onclick="OnButtonClick('ARM');" ><input type="button" value="DISARM" onclick="OnButtonClick('DISARM');" ><br>
		<input type="button" value="TAKEOFF" onclick="OnButtonClick('TAKEOFF');" ><input type="button" value="LAND" onclick="OnButtonClick('LAND');" ><br>

		<input type="button" value="GOTO" onclick="OnButtonClick('GOTO');">
		<input class='form-control' type="text" size="10" name="lat" id="lat" placeholder="Latitude" value="35.893246" />
		<input class='form-control' type="text" size="10" name="lon" id="lon" placeholder="Longitude" value="139.954909" />
		<input class='form-control' type="text" size="3" name="alt" id="alt" placeholder="Altitude" value="30" />
		</form>
	</div>
</div>
```
まずは新しく`<div>〜</div>`要素を作ります．
ボタンやエディットボックスを配置したいので，その中に`<form>〜</form>`を作ります．
formの中に，inputタグを置いていきます．

`<input type="button"〜`であればボタン型のフォームが，
`<input type="text"〜`であればエディットボックス型のフォームが生成されます．

GUIDED,RTL,ARM,DISARM,TAKEOFF,LAND,GOTOのコマンドのボタンを配置します．
(適宜改行`<br>`を入れる)
緯度/経度/高度を入力できるエディットボックスを配置します．

ボタン型のフォームには，`onclick="OnButtonClick(コマンド名)"`を追記してあります．
ボタンが押された時に発生するイベントが「onclickイベント」です．
onclickイベント発生時に呼び出す関数が`OnButtonClick`関数です．
これはscript部分に新たに自作した関数です．

つまり，PythonのGUIプログラムの時と同様に，「ボタンが押されたら，MQTTでPubする関数を呼び出す」というわけです．

なお，GOTOコマンドの時だけは，エディットボックスから緯度/経度/高度を抽出する作業も必要になりますね．


##script部

MQTTでPubするJSONデータを作るために，commandという連想配列を作っています．
これにコマンドを書き込んでPubするだけです．

```javascript:ボタンが押された時の処理
	// ドローン操作用のコマンドの連想配列
	var command = {
				"command":"None",
				"d_lat":"0",
				"d_lon":"0",
				"d_alt":"0",
	}
	
	// HTML上のボタンが押された時の処理
	function OnButtonClick(str) {
		command["command"] = str;	// 引数の文字列がそのままコマンドになる

		// GOTOのときは，緯度/経度/高度も取得してコマンドを作る
		if( str == "GOTO" ) {
			lat = document.getElementById("lat").value;
			lon = document.getElementById("lon").value;
			alt = document.getElementById("alt").value;
			command["d_lat"] = lat;
			command["d_lon"] = lon;
			command["d_alt"] = alt;
		}

		// JSON型にしてからMQTTでPublishする
		json_command = JSON.stringify(command);		// JSON型にする
		message = new Paho.MQTT.Message(json_command);		// MQTTのメッセージパケットを作る
		message.destinationName = "ctrl/001";	// トピック名を設定
		client.send(message);	// MQTTでPubする
		debug("SEND ON " + message.destinationName + " PAYLOAD " + message.payloadString);   //debugボックスに表示
	}
```
`OnButtonCliek`関数では，引数として受け取った文字列("GUIDED"とか"ARM"とか)を，
commandに書き込んでいます．

GOTOコマンドの時だけは，ID名'lat'や'lon'，'alt'と名のついた要素(=エディットボックスのid)から値(value)を取り出しています．
エディットボックスのフォームは，以下の様に書かれています．

```html:id名を与えれば，JavaScriptからアクセスできる
<input class='form-control' type="text" size="10" name="lat" id="lat" placeholder="Latitude" value="35.893246" />
<input class='form-control' type="text" size="10" name="lon" id="lon" placeholder="Longitude" value="139.954909" />
<input class='form-control' type="text" size="3" name="alt" id="alt" placeholder="Altitude" value="30" />
```
`id`として`lat`と`lon`と`alt`を持っています．
Webページ(`document`)上から特定のidを持つオブジェクトを入手する関数が`getElementById`です．

例えば`document.getElementById('lat').value`で緯度のエディットボックスに書き込まれている文字列(value)が得られます．


command連想配列への書き込みが完了したら，
それを素にJSON型のメッセージを作成します．`json_command = JSON.stringify(command);`
MQTTパケットを新しく作りますが，`message = new Paho.MQTT.Message(json_command);`
メッセージには宛先となるトピック名を追加するのを忘れてはいけません．`message.destinationName = "ctrl/001";	`
パケットを作り終わったら送信します．`client.send(message);	// MQTTでPubする`

最後におまけでLogs画面にデバッグ表示を書いています.


#実行結果

Webブラウザのアドレスバーに
http://localhost/map_button.html
と入力し，Enterしてページを表示させます．

こんな画面が表示されれば成功です．
ドローン操作用のボタンが並んでいることがわかります．

![Screenshot at 2019-05-30 18-39-48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256470/53c4a753-1665-e5f0-4afb-49457eb8a57e.png)

次は，ドローン側のシミュレータを起動しましょう．

```bash:ドローン側シミュレータを起動
$ python gui_sitl_pubsub.py 
```
起動が完了するとドローンから情報がPubされ，ブラウザ上にマーカーが現れます．
![map_sitl.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256470/b3fce74e-2c24-90c9-bebb-14135cca0b54.png)

#ドローンの操作

それでは，ドローンのマーカーをクリックしてポップアップを出したら，
ボタンを押してドローンを動かしてみましょう．

いつもの手順です．
(1)フライトモードをGUIDEDに変更．
(2)ARMする．
(3)TAKEOFF．
(4)GOTOで移動させる．
(5)LANDさせたり，RTLさせたり．

こんな風に動きます．
![Screenshot at 2019-05-30 18-55-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256470/6d11de4b-7b01-c92b-202a-0b0de6112c04.png)

#おわりに

Webブラウザでドローンの操縦もできるようになりました．

しかし，GOTOで移動ができるのは良いのですが，緯度と経度を数値で入力するのは大変です．
マウスでクリックした位置の座標を取れると便利ですね．
Leafletではそれが可能です．

次回は，マウスでクリックした位置へドローンを移動させてみます．