#はじめに
このページは,
の1ページです.
全体を見たい場合は上記ページへお戻りください.
#概要
前回は,PythonプログラムからMQTTでドローンを操作するコマンドを送ってみました.
しかし,WebブラウザとPythonのGUIプログラムを並べて使うというのは,イマイチ格好わるいです.
やはりWebブラウザからフルオペレーションできるほうが Cool! ですね.
今回はそれをやります.
#システム構成
ドローンのシミュレータは前回使った 「gui_sitl_pubsub.py」 です.
Webブラウザ用のプログラムを 「map_button.html」 として新しく作ります.
#Webブラウザ用のプログラム
ここ を右クリック-[名前を付けて保存]するか,
以下のコードをコピー&ペーストしてファイルを作成してください.
<!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を付けます.
$ sudo cp map_button.html /var/www/html/
#プログラム解説
前々回の marker_moving_rotated.html から大きく変化した部分だけ解説します.
##body部
前回のGUIプログラムの様に,ボタンをクリックするとコマンドを送信するようにします.
<!-- ドローンを操作するボタンを配置 -->
<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)を取り出しています.
エディットボックスのフォームは,以下の様に書かれています.
<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してページを表示させます.
こんな画面が表示されれば成功です.
ドローン操作用のボタンが並んでいることがわかります.
次は,ドローン側のシミュレータを起動しましょう.
$ python gui_sitl_pubsub.py
起動が完了するとドローンから情報がPubされ,ブラウザ上にマーカーが現れます.
#ドローンの操作
それでは,ドローンのマーカーをクリックしてポップアップを出したら,
ボタンを押してドローンを動かしてみましょう.
いつもの手順です.
(1)フライトモードをGUIDEDに変更.
(2)ARMする.
(3)TAKEOFF.
(4)GOTOで移動させる.
(5)LANDさせたり,RTLさせたり.
#おわりに
Webブラウザでドローンの操縦もできるようになりました.
しかし,GOTOで移動ができるのは良いのですが,緯度と経度を数値で入力するのは大変です.
マウスでクリックした位置の座標を取れると便利ですね.
Leafletではそれが可能です.
次回は,マウスでクリックした位置へドローンを移動させてみます.