#はじめに
このページは,
の1ページです.
全体を見たい場合は上記ページへお戻りください.
#概要
これまでは,ローカルなネットワーク(イントラネット)でドローンの操作を行ってきました.
しかしやはり,グローバルネットワーク(インターネット)を介して操作したいものです.
今回はVPS(バーチャル・プライベート・サーバー)でサーバー構築してみます.
しかし,
- OSにはUbuntuを使う
- MQTTブローカーとして mosquitto を使う
- Webサーバーとして Apache を使う
という点は,これまでLinux PCでやってきた作業と同じです.
#準備するもの
何かしらのVPSサービスに加入(購入)してください.
有名どころだと,以下の2つかと思います.
参考:VPS比較ランキング2019完全版!これ見て選べば初心者でも安心
今回の記事は,
「さくらのVPS CPU:仮想1Core メモリ:512MB SSD: 20GB」
のモデルで書きました.
##VPSについて
簡単にVPSの説明を.
**筆者個人の独断と偏見によるサーバーサービスのレベル分け**を書いておきます.
Linuxの知識がどの程度必要か,Linuxガチ度で分類しています
1位 専用サーバー
どこかにあるデータセンターのサーバーPCを1台まるごとレンタル.
「自分の手元ではなく,遠くにあるパソコン」なので,
操作をミスったら,遠くにいる管理者様に操作してもらうことになる.
設定失敗して起動しなくなったら,修正を有料でお願いするしかない.
OS再インストールも有料.
失敗の許されないサムライの仕様.ガチレベルMAX
2位 VPS(バーチャル・プライベート・サーバー)
物理PC上で動く仮想PC(エミュレーション)を1台レンタル.
「仮想専用サーバー」とも言う.
VMwareやVirtualBox等の仮想PCでLinuxを動かすのと同じ.
設定失敗したらWebのGUIでサクッとOS再インストール.
管理者様がいなくても必要なことは自分でできる.
Raspberry Piで喩えれば「起動しなくなったけど,SDカード書き換えてすぐ動いた」という感じ.
失敗してもすぐリスポーン繰り返せるヌルゲー仕様.
3位 レンタルサーバー
あるサーバーOS上のユーザアカウントを1つレンタル.
PC自体は他のユーザーと共用.
管理者権限は無いから,新しい機能のインストールとかはできない.
予め準備された世界の中で遊ぶだけのエンドユーザ仕様.
(準備された世界=お釈迦様の掌の上 と呼ぶ)
使いみちはWebページとメールアカウントぐらい?
4位 クラウド
Linuxとかソケット通信とかが分からない情弱でもWebGUIで簡単に使えますように(祈り
機械学習とかなんでも使えて至れり尽くせり.
ただし,従量課金なのでミスったら莫大な請求が...
というわけで,
Raspberry Piの様なLinuxボード経験者であれば,VPSは十分に運用できます.
昔の組込Linuxボードだと,起動しなくなった際にイメージを焼き直すのが大変でした.
OSのインストールを何度でもやり直せる,すなわち何度でも失敗できる,というのはVPSの強みです.
##大手クラウドサービス
もちろん,大手クラウドサービスのVPS機能を使っても良いと思います.
- AWS(Amazon Web Services) - Amazon EC2 / Lightsail
- GCP(Google Cloud Platform) - Google Compute Engine
- Azure(Microsoft Azure) - Microsoft Virtual Machine
が,トラフィックで従量課金というのは,
昔で言う「パケ死」が怖くて筆者はまだ試していません((((;゚Д゚))))
参考:クラウド破産について
#システム構成
システム構成は以下の図の様になります.
ドローンの代わりにシミュレータ(dronekit-sitl)を使っています.
#OSの再インストール(さくらのVPS)
ここでは,さくらのVPSを使って例を示します.
他のVPSサービスを使う時は,そのサービス会社の管理システムを使ってください.
さくらのVPS登録完了直後は,何かのOSがインストールされています.(たぶんCentOS)
本企画ではUbuntuを使っていますので,ディストリビューションが違うとややこしいですね.
なので,まずはいきなりOSを再インストールしましょう.
(サーバーがうまく動かなくなった時,ハックされて乗っ取られた時なども同様の方法で再インストールします)
Ubuntu 18.04LTSはまだ無いので,16.04LTSにします.
**初期ユーザー名はubuntuに固定**です.
パスワードには,さくらインターネットから渡されたパスワードを使っておくと管理が楽です.
スタートアップスクリプトの項目では,筆者は[Ubuntu_ufw]を選択しました.
ファイアーウォール(iptables)を有効にして,許可したポートしかアクセスできないようにするためです.
[利用しない]を選択してもかまいませんが,グローバルIPを持ったサーバが丸裸状態になるのでハッキングに気をつける必要があります.
手動でファイアウォールをインストールすべきです.
設定が完了したら[設定内容を確認する]ボタンを押し,
確認画面で[インストールを実行]を押してインストールを開始します.
しばらく待っていると,インストールが完了してUbuntuが起動します.
後は,TeraTermやPuttyといったSSHクライアントで接続してログインします.
また,さくらの管理画面にはシリアルコンソールがあるので,
ファイアーウォールのポート設定をミスった時などの緊急時にはこれを利用して管理することもできます.
#システムの更新
まずはaptのリポジトリのリストを最新のものに更新(update)し,
更新があるソフトをアップグレード(upgrade)します.
$ sudo apt update
$ sudo apt upgrade -y
#MQTTブローカーのインストール
Mosquittoのインストールはaptで一発です.
関連ツールも一応インストールしておきます.
$ sudo apt install mosquitto
$ sudo apt install mosquitto-clients
WebブラウザでMQTTを直接送受信することはできないので,
と同様にMQTT over WebSocketの設定をします.
mosquittoの設定ファイル
/etc/mosquitto/mosquitto.conf
を編集します.
その際,管理者権限が必要なのでsudoを付けます.
$sudo nano /etc/mosquitto/mosquitto.conf
mosquitto.conf
ファイルの先頭に,以下の行を追加します
listener 1883
listener 15675
protocol websockets
設定が終わったらエディタを終了します.
(nanoの場合,Ctrl+oでファイルを保存し,Ctrl+xで終了)
その後,mosquittoサービスを再起動します.
$sudo systemctl restart mosquitto
#Webサーバーのインストール
次に,Webサーバ(apache2)をインストールします.
(taskselでLAMP serverをインストールしてもいいです)
$sudo apt install apache2
OSインストール時に[Ubuntu_ufw]を選択した場合は,
この時点ではiptablesによって80番ポートが閉じられているので,
Webブラウザでサーバーのページを開こうとしても,何も表示されません.
#ポートの開放
OSインストール時に[Ubuntu_ufw]を選択した場合は,
iptables(ファイアーウォール)が有効になっていて,
かつufw(iptablesのかんたん管理ツール)もインストール済みです.
以下のコマンドを打って,mosquitoとapache2のためのポートを開けておきましょう.
$ sudo ufw allow 1883/tcp
$ sudo ufw allow 15675/tcp
$ sudo ufw allow "Apache Full"
MQTTに必要な1883番ポート,
MQTT over WebSocketに必要な15675番ポート,
Apacheに必要な80と443番ポートを指定しています.
Apache Full
という名前指定は,
$ sudo ufw app list
というコマンドを打つと,予め登録されているネットワークサービスが表示されます.
$ sudo ufw app list
Available applications:
Apache
Apache Full
Apache Secure
OpenSSH
・Apache
は80番ポート
・Apache Full
は80と443番ポート
・Apache Secure
は443番ポート
・OpenSSH
はsshの22番ポート
今回はhttpsは使いませんが,とりあえずApache Full
を使いました.
最後に,希望するポートが開いているかどうかを確認します.
$ sudo ufw status
以下の様に表示されれば成功です.
$ sudo ufw status
Status: active
To Action From
-- ------ ----
22 ALLOW Anywhere
1883/tcp ALLOW Anywhere
15675/tcp ALLOW Anywhere
Apache Full ALLOW Anywhere
22 (v6) ALLOW Anywhere (v6)
1883/tcp (v6) ALLOW Anywhere (v6)
15675/tcp (v6) ALLOW Anywhere (v6)
Apache Full (v6) ALLOW Anywhere (v6)
正しく設定できているのを完了したら,
Webブラウザでhttp://(VPSサーバのIPアドレス)/を開いてみてください.
ブラウザにこんなページが表示されれば,apache2のインストールもポートの開放も成功です.
#HTMLのフォルダにファイルを置く
Webサーバのホームディレクトリである/var/www/htmlへ必要なファイルをコピーする必要があります.
以下の表のように,必要なファイルが沢山あります.
用途 | ファイル名 |
---|---|
WebSocket | mqttws31.js |
Leafletプラグイン | MovingMarker.js,leaflet.rotatedMarker.js,leaflet.contextmenu.css,leaflet.contextmenu.js |
ドローンアイコン | quad_x-90.png |
HTMLファイル本体 | start.html |
それぞれ個別にダウンロードしてコピーするのは大変なので,圧縮ファイルを用意しました.
以下のコマンドでダウンロードと解凍を行ってください.
$ cd ~
$ wget https://github.com/DCoJA/OpenUVTM/raw/master/openuvtm_set.tar.gz
$ sudo tar -xvf openuvtm_set.tar.gz -C /var/www/html/
tar
コマンドは-C
オプションで解凍先を指定できます.
また,/var/www/html/
へのファイルコピーにはsudo権限が必要です.
#ドローン側プログラム(dronekit-sitl)
今回はローカルの母艦でdronekit-sitlを起動して,VPSサーバーへMQTTでPubします.
具体的には
MQTTでドローンにコマンドを送る
で作ったプログラムの改変です.
ここ を右クリック-[名前を付けて保存]するか,
以下のコードをコピー&ペーストしてファイルを作成してください.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import time # ウェイト関数time.sleepを使うために必要
from subprocess import Popen # subprocessの中から、Popenをインポート
from signal import signal, SIGINT # Ctrl+C(SIGINT)の送出のために必要
from dronekit import connect # connectを使いたいのでインポート
from dronekit import VehicleMode # VehicleModeも使いたいのでインポート
from dronekit import LocationGlobal, LocationGlobalRelative # ウェイポイント移動に使いたいのでインポート
import Tkinter # GUIを作るライブラリ
import paho.mqtt.client as mqtt # MQTT送受信
import json # JSON<-->辞書型の変換用
#==dronekit-sitl の起動情報================================
# example: 'dronekit-sitl copter --home=35.079624,136.905453,50.0,3.0 --instance 0'
sitl_frame = 'copter' # rover, plane, copterなどのビークルタイプ
sitl_home_latitude = '35.894087' # 緯度(度) 柏の葉キャンパス駅前ロータリー
sitl_home_longitude = '139.952447' # 経度(度)
sitl_home_altitude = '17.0' # 高度(m)
sitl_home_direction = '0.0' # 機首方位(度)
sitl_instance_num = 0 # 0〜
# コマンドライン入力したい文字列をリスト形式で作成
sitl_boot_list = ['dronekit-sitl',sitl_frame,
'--home=%s,%s,%s,%s' % (sitl_home_latitude,sitl_home_longitude,sitl_home_altitude,sitl_home_direction),
'--instance=%s'%(sitl_instance_num)]
#connection_stringの生成
connection_string = 'tcp:localhost:' + str(5760 + int(sitl_instance_num) * 10 ) # インスタンスが増えるとポート番号が10増える
#== MQTTの情報,Pub/Subするトピック =======================
#mqtt_server = 'IPアドレスをここに書く' # 適宜サーバーのIPアドレスを指定すること
mqtt_server = '160.16.229.145' # 筆者のテストサーバーのIPアドレス
mqtt_port = 1883
mqtt_pub_topic = 'drone/001' # Publish用のトピック名を作成
mqtt_sub_topic = 'ctrl/001' # Subscribe用のトピック名を作成
#== MQTTに送信するJSONのベースになる辞書 ===================
drone_info = {
"status":{
"isArmable":"false",
"Arm":"false",
"FlightMode":"false"
},
"position":{
"latitude":"35.0000",
"longitude":"135.0000",
"altitude":"20",
"heading":"0"
}
}
#== MQTTで受信するコマンドのベースになる辞書 ================
drone_command = {
"IsChanged":"false",
"command":"None",
"d_lat":"0",
"d_lon":"0",
"d_alt":"0"
}
#== メイン関数 ============================================
def main(args):
#==Tkinterのウィンドウを作る===============================
root = Tkinter.Tk() # ウィンドウ本体の作成
root.title(u'Dronekit-SITL Monitor') # ウィンドウタイトルバー
root.geometry('400x550') # ウィンドウサイズ
# Status
frame0 = Tkinter.Frame(root,pady=10)
frame0.pack()
Label_status = Tkinter.Label(frame0,font=("",11),text="Status:")
Label_status.pack(side="left")
EditBox_status = Tkinter.Entry(frame0,font=("",11),justify="center",width=15)
EditBox_status.pack(side="left")
# IsArmable
frame1 = Tkinter.Frame(root,pady=10)
frame1.pack()
Label_armable = Tkinter.Label(frame1,font=("",11),text="IsArmable:")
Label_armable.pack(side="left")
EditBox_armable = Tkinter.Entry(frame1,font=("",11),justify="center",width=15)
EditBox_armable.pack(side="left")
# ARM/DISARM
frame2 = Tkinter.Frame(root,pady=10)
frame2.pack()
Label_arm = Tkinter.Label(frame2,font=("",11),text="Armed:")
Label_arm.pack(side="left")
EditBox_arm = Tkinter.Entry(frame2,font=("",11),justify="center",width=15)
EditBox_arm.pack(side="left")
# フライトモード
frame3 = Tkinter.Frame(root,pady=10)
frame3.pack()
Label_flightmode = Tkinter.Label(frame3,font=("",11),text="Flight mode:")
Label_flightmode.pack(side="left")
EditBox_flightmode = Tkinter.Entry(frame3,font=("",11),justify="center",width=15)
EditBox_flightmode.pack(side="left")
# 緯度
frame5 = Tkinter.Frame(root,pady=10)
frame5.pack()
Label_lat = Tkinter.Label(frame5,font=("",11),text="Latitude:")
Label_lat.pack(side="left")
EditBox_lat = Tkinter.Entry(frame5,font=("",11),justify="center",width=15)
EditBox_lat.pack(side="left")
# 経度
frame6 = Tkinter.Frame(root,pady=10)
frame6.pack()
Label_lon = Tkinter.Label(frame6,font=("",11),text="Longitude:")
Label_lon.pack(side="left")
EditBox_lon = Tkinter.Entry(frame6,font=("",11),justify="center",width=15)
EditBox_lon.pack(side="left")
# 高度
frame7 = Tkinter.Frame(root,pady=10)
frame7.pack()
Label_alt = Tkinter.Label(frame7,font=("",11),text="Altitude:")
Label_alt.pack(side="left")
EditBox_alt = Tkinter.Entry(frame7,font=("",11),justify="center",width=15)
EditBox_alt.pack(side="left")
# 方位
frame8 = Tkinter.Frame(root,pady=10)
frame8.pack()
Label_dir = Tkinter.Label(frame8,font=("",11),text="Bearing:")
Label_dir.pack(side="left")
EditBox_dir = Tkinter.Entry(frame8,font=("",11),justify="center",width=15)
EditBox_dir.pack(side="left")
# InstanceNumber
frame4 = Tkinter.Frame(root,pady=10)
frame4.pack()
Label_num = Tkinter.Label(frame4,font=("",11),text="Instance Number:")
Label_num.pack(side="left")
EditBox_number = Tkinter.Entry(frame4,font=("",11),justify="center",width=15)
EditBox_number.pack(side="left")
# Pubトピック名
frame9 = Tkinter.Frame(root,pady=10)
frame9.pack()
Label_pubtopic = Tkinter.Label(frame9,font=("",11),text="Publish Topic:")
Label_pubtopic.pack(side="left")
EditBox_pubtopic = Tkinter.Entry(frame9,font=("",11),justify="center",width=30)
EditBox_pubtopic.pack(side="left")
# Subトピック名
frame10 = Tkinter.Frame(root,pady=10)
frame10.pack()
Label_subtopic = Tkinter.Label(frame10,font=("",11),text="Subscribe Topic:")
Label_subtopic.pack(side="left")
EditBox_subtopic = Tkinter.Entry(frame10,font=("",11),justify="center",width=30)
EditBox_subtopic.pack(side="left")
#==dronekit-sitlの起動====================================
p = Popen(sitl_boot_list) # サブプロセスの起動
time.sleep(1) # 起動完了のために1秒待つ
#==フライトコントローラ(FC)へ接続==========================
vehicle = connect( connection_string, wait_ready=True ) # 接続
#==MQTTのSubscribe関数====================================
def on_message(client, userdata, msg):
recv_command = json.loads(msg.payload)
# 受信メッセージをコマンド辞書にコピー、その際に変更フラグを付加
drone_command["IsChanged"] = "true" # 届いた際にtrueにし,コマンドを処理したらfalseにする
drone_command["command"] = recv_command["command"]
if drone_command["command"] == "GOTO":
drone_command["d_lat"] = recv_command["d_lat"]
drone_command["d_lon"] = recv_command["d_lon"]
drone_command["d_alt"] = recv_command["d_alt"]
#==MQTTの初期化===========================================
client = mqtt.Client() # クラスのインスタンス(実体)の作成
client.connect( mqtt_server, mqtt_port, 60 ) # 接続先は自分自身
client.subscribe( mqtt_sub_topic )
client.on_message = on_message
client.loop_start() # 通信処理スタート
#==1秒おきに画面表示を更新する関数=========================
def redraw():
# ステータス、Arming関連、フライトモード情報の更新
EditBox_status.delete(0,Tkinter.END) # 前の文字列を削除
EditBox_status.insert(Tkinter.END, str(vehicle.system_status.state) ) # 新しい文字列を書き込む
EditBox_armable.delete(0,Tkinter.END)
EditBox_armable.insert(Tkinter.END, str(vehicle.is_armable) )
EditBox_arm.delete(0,Tkinter.END)
EditBox_arm.insert(Tkinter.END, str(vehicle.armed) )
EditBox_flightmode.delete(0,Tkinter.END)
EditBox_flightmode.insert(Tkinter.END, str(vehicle.mode.name) )
# 緯度/経度/高度/方位の更新
EditBox_lat.delete(0,Tkinter.END)
EditBox_lat.insert(Tkinter.END, str(vehicle.location.global_frame.lat) )
EditBox_lon.delete(0,Tkinter.END)
EditBox_lon.insert(Tkinter.END, str(vehicle.location.global_frame.lon) )
EditBox_alt.delete(0,Tkinter.END)
EditBox_alt.insert(Tkinter.END, str(vehicle.location.global_frame.alt) )
EditBox_dir.delete(0,Tkinter.END)
EditBox_dir.insert(Tkinter.END, str(vehicle.heading) )
# 起動インスタンス番号、MQTTのトピック名の更新
EditBox_number.delete(0,Tkinter.END)
EditBox_number.insert(Tkinter.END, str(sitl_instance_num) )
EditBox_pubtopic.delete(0,Tkinter.END)
EditBox_pubtopic.insert(Tkinter.END, mqtt_pub_topic )
EditBox_subtopic.delete(0,Tkinter.END)
EditBox_subtopic.insert(Tkinter.END, mqtt_sub_topic )
#==Publishするデータを作る===============================
drone_info["status"]["isArmable"] = str(vehicle.is_armable) # ARM可能か?
drone_info["status"]["Arm"] = str(vehicle.armed) # ARM状態
drone_info["status"]["FlightMode"] = str(vehicle.mode.name) # フライトモード
drone_info["position"]["latitude"] = str(vehicle.location.global_frame.lat) # 緯度
drone_info["position"]["longitude"] = str(vehicle.location.global_frame.lon)# 経度
drone_info["position"]["altitude"] = str(vehicle.location.global_frame.alt) # 高度
drone_info["position"]["heading"] = str(vehicle.heading) # 方位
#==MQTTの送信===========================================
json_message = json.dumps( drone_info ) # 辞書型をJSON型に変換
client.publish("drone/001", json_message ) # トピック名は以前と同じ"drone/001"
# コマンドに対する処理
if drone_command["IsChanged"] == "true":
# GUIDEDコマンド
if drone_command["command"] == "GUIDED":
print("# Set GUIDED mode")
vehicle.mode = VehicleMode("GUIDED")
# RTLコマンド
if drone_command["command"] == "RTL":
print("# Set RTL mode")
vehicle.mode = VehicleMode("RTL")
# ARMコマンド
if drone_command["command"] == "ARM":
print("# Arming motors")
vehicle.armed = True
# DISARMコマンド
if drone_command["command"] == "DISARM":
print("# Disarming motors")
vehicle.armed = False
# TAKEOFFコマンド
if drone_command["command"] == "TAKEOFF":
print("# Take off!")
aTargetAltitude = 20.0
vehicle.simple_takeoff(aTargetAltitude) # Take off to target altitude
# LANDコマンド
if drone_command["command"] == "LAND":
print("# Set LAND mode...")
vehicle.mode = VehicleMode("LAND")
# GOTOコマンド
if drone_command["command"] == "GOTO":
print("# Set target position.")
if drone_command["d_lat"] == "0":
drone_command["d_lat"] = str( vehicle.location.global_frame.lat ) # 緯度
if drone_command["d_lon"] == "0":
drone_command["d_lon"] = str( vehicle.location.global_frame.lon ) # 経度
if drone_command["d_alt"] == "0":
drone_command["d_alt"] = str( vehicle.location.global_frame.alt ) # 高度
point = LocationGlobalRelative(float(drone_command["d_lat"]), float(drone_command["d_lon"]), float(drone_command["d_alt"]) )
vehicle.simple_goto(point, groundspeed=5)
# コマンドは読み終えたので、フラグを倒す
drone_command["IsChanged"] = "false"
drone_command["command"] = "None"
root.after(1000, redraw ) # 1秒後に自分自身を呼び出す
#==Tkinterの時間実行の機能after関数を使う===================
root.after(1000, redraw ) # 最初に1回だけは本文で呼び出す必要がある.
#==Tkinterメインループ====================================
# Xボタンを押すまでこの関数がブロックする
root.mainloop()
#==ここから終了処理========================================
# MQTT終了
client.loop_stop()
# フライトコントローラとの接続を閉じる
vehicle.close()
# サブプロセスにもSIGINT送信
p.send_signal(SIGINT)
p.communicate()
time.sleep(1) # 終了完了のために1秒待つ
return 0
# このpyファイルがimportされたのではなく,scriptとして実行された時
if __name__ == '__main__':
sys.exit(main(sys.argv)) # ここでmain関数を呼ぶ.argvはC言語と同様にコマンドライン引数
プログラムは端末から実行します.
$python gui_sitl_pubsub_vps.py
##プログラム解説
変更点は36行目のMQTTサーバーの指定だけです.
以前はここをlocalhost
として自分自身を指定していました.
今回はVPSサーバーのIPアドレスを書きます.
参考までに,筆者のVPSサーバーのIPアドレスを書いておきましたが,
自分のサーバーに書き換えて使ってください.
#== MQTTの情報,Pub/Subするトピック =======================
#mqtt_server = 'IPアドレスをここに書く' # 適宜サーバーのIPアドレスを指定すること
mqtt_server = '160.16.229.145' # 筆者のテストサーバーのIPアドレス
mqtt_port = 1883
#動作確認
Webブラウザで,http://(VPSサーバーのIPアドレス)/start.html
を開けば,実行結果を見ることができます.
参考までに,筆者のサーバーのアドレスを記載しておきます.
(常時ドローンが飛んでいるとは限りませんが)
今までと同様に,ARM/DISARM,Takeoff/Land,緯度経度指定移動ができることを確認しましょう.
#(注意)サーバーセキュリティについて
今回のサーバー設定は「とりあえずの入門」としてiptablesとufwを使いました.
しかしこれは最低限度のセキュリティ対策です.
とりあえず施すべき対策としては,以下が考えられます.
・sshのポートを22番から変更するべき
sshで22番を使うことがわかっているので,
ユーザー名root
,admin
,ubuntu
とかでログイン試行してくる攻撃はよくあります.
したがって,とりあえず22番以外のポートに変更するか,
SSL/TLSの暗号化鍵を使う様に変更するべきです.
・MQTTのポートを1883番から変更するべき
sshと同様に,MQTTの1883番もIoTでよく使われるので,
1883番にゴミパケットを投げまくって落とす攻撃がよくあります.
mosquittoが使うポート番号も1883以外にしておくべきでしょう.
さらには,fail2banを使って連続攻撃を検知し,一時的にbanできるようにすると良いでしょう.
#おわりに
今回はVPSサーバーにドローン(シミュレータ)の情報を投げて操作できるようにしました.
次回は,実機をインターネット越しに操作してみます.