2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-05-30

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

ドローン操作システムを作ろう

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

#概要

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

#システム構成

browser_operation.png

ドローンのシミュレータは前回使った 「gui_sitl_pubsub.py」 です.

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

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

ここ を右クリック-[名前を付けて保存]するか,
以下のコードをコピー&ペーストしてファイルを作成してください.

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を付けます.

apacheのフォルダへコピー
$ sudo cp map_button.html /var/www/html/

#プログラム解説

前々回の marker_moving_rotated.html から大きく変化した部分だけ解説します.

##body部

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

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するだけです.

ボタンが押された時の処理
	// ドローン操作用のコマンドの連想配列
	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)を取り出しています.
エディットボックスのフォームは,以下の様に書かれています.

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としてlatlonaltを持っています.
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

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

ドローン側シミュレータを起動
$ python gui_sitl_pubsub.py 

起動が完了するとドローンから情報がPubされ,ブラウザ上にマーカーが現れます.
map_sitl.png

#ドローンの操作

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

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

こんな風に動きます.
Screenshot at 2019-05-30 18-55-00.png

#おわりに

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

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

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

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?