Tello で Object tracking
このブログは、大國魂(ITブログ) Advent Calendar 2018 - Adventar の8 日目です。
Tello とは
わずか80gの おもちゃドローンですがホバリングの安定性や操作性がとても素晴らしいです。十数回、壁に激突していますが元気です。そんなTelloはプログラミングで動かすことができます。
https://store.dji.com/jp/shop/tello-series
Tello を プログラミングで動かすには
Tello の公式サイトにある SDKのドキュメントに使用できるコマンドが記載されています。 今回使用しているコマンドは SDK v1.3 がベースになります。
- Tello SDK Documentation (v1.3)
https://dl-cdn.ryzerobotics.com/downloads/Tello/20180910/Tello%20SDK%20Documentation%20EN_1.3.pdf
Tello SDK では Tello とWifi で接続し UDP Socketで 通信します。アプリケーションは テキストコマンドを送信することで Tello をコントロールできるようになります。
今回作ったアプリケーション
コードは GitHub に上げました。 github.com
実際の Tello の動き
動画では黄色い物体を追跡しています。
もし試しに使われる際は、安全には十分に注意して周りに人がいない場所で飛ばしてください。
Telloの可動範囲を制御する設定は、設定ファイル(tello.cfg) の [tacking] セクションにある position_limit です。トラッキング処理で追跡対象をロストした場合、意図せずTelloが動き続ける可能性があるので、値を大きくする際は十分に気を付けてください。
[tracking] # !! important !! set the motion limit range of Tello for your safety # (x, y, z (cm), rotate (dgree)) position_limit = (300, 300, 300, 180)
アプリケーションの構成
WebUIと Tello コントロール用の API 部分は Flaskで 実装しました。
- ブラウザ (Web UI ) から 各ボタンをクリックすると特定の URI に POSTでリクエストします。
- リクエストを受け取った Flask App は、Tello SDK を利用して UDP Socket 経由でコマンドを送信し、その結果を受信します。
- コマンドの結果は、Flask から ブラウザへ 非同期 に反映されます。
- Video Streaming では Tello からの Streaming を OpenCV の VideoCapture で 受けて ブラウザ で 表示します。
- Object tracking ではOpenCV を使って一定の間隔でトラッキング処理を行います。さらにその結果をTello を動作させるプログラム(tellolib) に渡し、Tello SDK と連携させます。
Object tracking と Tello との連携
Object tracking の動作は OpenCV チュートリアル の サンプルコードを使って確認することができます。 このトラッキング の結果 と連携して Tello を動かすための仕組みは以下のようなものです。
- 検知した物体は 青枠で囲まれます。
- 赤枠は Tello が物体を追跡する境界です。赤枠を対象物体が超えるとその方向にTello が動くようにコマンドを送信します。結果的に赤枠内に収まるように動作させます。
- 黄色枠は、物体の奥行きを図る基準として使います。対象物体の青枠面積と基準とする黄色枠面積の平方根の割合によって Tello を前後に動かしています。
- Web UI では トラッキングモード を リアルタイムで変更できるようにしています(Stream only, Test mode , Tracking mode) 。
技術的な話(悩んだところ)
トラッキング処理のフレーム調整
Tello の ストリーミング をそのままトラッキング処理をするとカクつきが激しいので フレームをリサイズ (320 * 240)してトラッキング処理に渡しています。
camera.py
def get_frame(self, stream_only, is_test, speed): . . ret, self.frame = self.video.read() self.frame = cv2.resize( cv2.flip(self.frame, flipcode), (frame_prop[0], frame_prop[1])) if not self.stream_only: if not self.t.is_alive(): self.t = threading.Thread( target=self._tracking, args=(ret, self.frame)) self.t.start()
トラッキング処理の間隔
前回のロボットアームの時とは違い、Tello では一定間隔でトラッキング処理を実行しています。つまりフレーム毎にトラッキング処理を実行するのではなく、意図的にフレームをスキップしています。理由は、Tello は 動作するタイミングで通常のストリーミングでもフレームが飛ぶ傾向があるためです。例えば 短時間に大量のコマンドを送信して Tello が動作しようとすると、フレーム飛びが多発しトラッキングどころではなくなります。トラッキング処理間隔は tello.cfg の [tello] セクションで指定できます。
[tracking] track_interval = 0.06
Tello へのコマンド送信の間隔
トラッキング処理とは別にコマンド送信の間隔も制御できるようにしています。これは、物体をTelloが追跡するときに動き過ぎないようにするための設定です。 下の例では垂直、水平、前後動作は 1.0 秒 間隔、回転動作は 0.1 秒間隔です。 例えば Tello に "left 20" とコマンドを送ると、左方向に 20cm 動くわけですが 1.0 秒間隔を置く(スレッドが生きているので他のスレッドが起動しない)ので、連続して送信するのを防ぎます。
[tello] move_interval = 1.0 rotate_interval = 0.1
でも、この間隔を大きくし過ぎると動作が緩慢になってしまいます。色々なパターンを試しましたがベストな設定が分かっていません。理想はこういった調整をしなくてもいい感じに動いてくれることなので、そもそものこのやり方が良くないんだと思っています。
水平方向と回転の動きの使い分け
Tello は 上下左右、前後、回転の動作をしますが、対象物体が水平方向に動いたとき、左右に動かすのか、回転させるのかは迷いました。現時点では、物体の動いた距離によって動作を変えるようにしています。トラッキングの結果から「1フレーム前の位置」と「現在の位置」の差分の割合 (move_ratio) を計算し、それが閾値を下回る場合は回転、そうでない場合は水平方向の動作としました。
camera.py
def _calc_move_ratio(self, track_window, track_window0): x, y, w, h = track_window x0, y0, w0, h0 = track_window0 diff = (x0 - x, y0 - y, w0 - w, h0 - h) move_ratio = (round(diff[0] / self.video_prop[0], 5), round(diff[1] / self.video_prop[1], 5)) return move_ratio
tellolib.py
def motion(self, track_window, track_area_ratio, move_ratio, margin_window, is_test): . . if x < xmin: if abs(move_ratio[0]) < move_ratio_threshold[0]: if self.rotate < self.rotate_limit: command = "cw" elif self.sent_command not in ('cw', 'ccw'): if self.xpos > self.xpos_limit * -1: command = "right" . .
ブラウザ と Flask の非同期通信
ブラウザのリロードなし(非同期)で、Tello からのレスポンスを表示したりトラッキングモードを切り替えたいと思って調べていたら、 Ajax の post を使えばできそうだと分かりました。 今さらながら jQueryの手軽さを体感しました。 今後色々と使えそうです。
tello.js
$(function () { $('.btn').on('click', function () { var command = JSON.stringify({ "command": $('#' + $(this).attr('id')).val() }); if (info_cmd.includes(JSON.parse(command).command)) { url = '/info'; . . post(url, command); }); function post(url, command) { $.ajax({ type: 'POST', url: url, data: command, contentType: 'application/json', timeout: 10000 }).done(function (data) { < 成功したときの処理 > }).fail(function (jqXHR, textStatus, errorThrown) { < 失敗したときの処理 > }); return false; } });
Tello からのレスポンスの反映
Tello へ送信したコマンドのレスポンスは非同期で(スレッドで)受信します。コマンドによって応答のタイミングが違うので、送信したコマンドと結果が必ずしも一致しないことがあります。送信コマンドと応答を紐づける術がわからないので今のところどうしようもない部分です。
app.py (Tello からの レスポンスを受け取っている部分)
""" Create a UDP socket to send and receive message with tello """ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: host = '0.0.0.0' port = 9000 s.bind((host, port)) def recv(): global tello_response while True: try: tello_response, server = s.recvfrom(1518) tello_response = tello_response.decode(encoding="utf-8") logger.info("res:{}".format(tello_response)) except Exception: print('\nExit . . .\n') break recvThread = threading.Thread(target=recv) recvThread.start()
Tello からの Video Stream 受信
Tello SDK のドキュメントには以下のようにあります。
Receive Tello Video Stream Tello IP: 192.168.10.1 ->> PC/Mac/Mobile UDP Server: 0.0.0.0 UDP PORT:11111 Remark4: Set up a UDP server on PC, Mac or Mobile device and listen the message from IP 0.0.0.0 via UDP PORT 11111.
Remark5: Do Remark2 if you haven’t. Then send “streamon” command to Tello via UDP PORT 8889 to start the streaming.
Telloから Video Stream を受信するには "streamon" コマンドを送信した後、 UDP port 11111 で ストリーミングを待ち受ける必要があります。
UDP Socketを作って bind する必要があると思ったのですが、明示的な bind の記述は必要なく以下のコードで動作します。 cv2.VideoCapture('udp://127.0.0.1:11111') が実行されるタイミングで UDP 0.0.0.0 :11111 でリッスンします。OpenCV の ドキュメント や ソース を 見ても All Address でリッスンする部分は確認できず、今でもしっかり理解できていません。
camera.py
class VideoCamera(object): def __init__(self, socket, algorithm, target_color, stream_only, is_test, speed): self.video = cv2.VideoCapture('udp://127.0.0.1:11111')
Tello コントロール用の REST な API について
API Key のようなアクセス制限があるわけではなく APIと呼ぶのはどうかと思いますが、Tello を動かす URI を提供しているという点で APIと書きました。一応 curl でも Telloのステータスを取ることができます。
URI | 説明 |
---|---|
/info | バッテリー、スピード、温度などの Tello コマンドを受付、結果を返す |
/tellooo | Tello のコントローラコマンド受付 |
/tracking | トラッキングモード切替 |
$ curl -s -X POST localhost:5000/info -H "Content-Type: application/json" -d '{"command": "battery?"}' { "ResultSet": "{\"command\": \"battery?\", \"result\": \"26\", \"connected\": false, \"streamon\": false}" }
まとめ
正直なところ Telloでのトラッキングの精度は いまいち です。ロボットアームで追跡処理を実装した時に 解像度: 320(w) x 240(h) フレームレート: 16 fps でまずまずの精度だったので、このプログラムでも フレームのリサイズ (320 * 240 ) やトラッキング間隔 (0.06 ≒ 16fps相当) で設定していますが、Tello が動作したタイミングでフレームが飛んでしまい 追跡対象をロストすることが多いです。Telloに「追跡させる」というより、「追跡しやすいように対象物体を動かしてあげる」という感じです。
初めはCLIで作っていたのですがObject Tracking を別プロセスで起動したり都度ブラウザ開いたりが面倒なのでWeb UIに統一しました。結果としてリアルタイムでトラッキングモードを切り替えできるようになったので満足しています。
本来の機能を活かしつつ、自分なりの違った楽しみ方ができるのがプログラマブルな おもちゃの良いところだと思います。Tello に費やした膨大な時間が報われますように。
参考
- FlaskでVideo Streamingする仕組み
https://blog.miguelgrinberg.com/post/video-streaming-with-flask https://github.com/miguelgrinberg/flask-video-streaming
- Object Tracking (camshift)
MeArmPi で Object tracking
MeArmPi で Object tracking
対象物体を検知し追跡するロボットです。ロックオンするとグリップが動きます。
コードは GitHub に上げています。
MeArmPi とは?
MeArmPi は ラズパイで動くロボットアームキットです。ジョイスティックでの操作やブラウザからプログラミングを組んで動かすことができます。
MeArmPiには、カメラは付属していないので、カメラモジュール(PiCamera)を取り付けました。
環境
OpenCV : 3.4.3.18
MeArmPi : image_2018-01-18-HeadlessPi.zip http://downloads.mime.co.uk/MeArmPi-latest
Python 3.5 ( Raspberry Pi のバンドル)
仕組み
ブラウザから見るロボットカメラの画像です。「黄色いもの」を検知/追跡しています。 追跡する物体の位置が赤枠の境界を超えると枠内に戻す方向にロボットアームを動かすという仕組みです。 下は物体の検出を表す画像(ヒストグラムの逆投影)です。物体が白く見えます。
Python でサーボを動かすために
コードは以下を使わせてもらいました。
- MeaArmPi TechnicalOverview
実際にやる際は、このドキュメントにある sample コードを実行してサーボが意図した角度に動くかどうか十分に確認することをお勧めします。
- Mearm(Python library)
https://gist.github.com/bjpirt/9666d8c623cb98e755c92f1fbeeb6118
動く範囲を制限するために MeArm クラスで maxAngle が設定されていますが、私の MeArmPi ではこの制限では足りずサーボモーターがアームの可動域を超えて動こうとするのでモーターが過度に熱くなってしまいました。安全のためさらに±25度の制限をかけています。
mearm.py
safe_angle = 25 def moveToAngle(self, angle): """prevent mearmpi from moving so far (angle: +- safe_angle)""" if angle > self.maxAngle - safe_angle: angle = self.maxAngle - safe_angle if angle < self.minAngle + safe_angle: angle = self.minAngle + safe_angle self.currentAngle = angle self.updateServo()
ロボットアームとの連携
あとは物体の動きに合わせてアームを動かす実装です。初めは動いた方向と距離からサーボを回転させる角度を計算すれば良いだろうと考えてました。 実際やってみると、それでは物体が動いた方向にアームを動かした後、ロボットのカメラにとっては物体は中央に戻ったように見えるわけで、ロボットアームは行ったり来たりと誤動作(ピンポン動作)を繰り返します。また角度を割り出す方法も簡単ではありませんでした。
そこで考えを改めてフレームに一定の境界を設定して、それを超えたらその方向に少しずつアームを動かす単純な実装にしました。結果として赤枠内(上の画像参照)に収まる動きが実現できます。赤枠はフレームの端から余白をどれくらいとるのか frame_margin というパラメーターで調整しています。
- config.ini
[camera] # define window boundary at which robot arm starts moving. # margin window is drawn red line in the frame. frame_margin = 0.12
それでもアームが動きすぎるとピンポン動作が発生しますし、角度が小さすぎすると動作が緩慢になるためちょうどいいパラメーターを調整する必要があります。 パラメータは [mearm] セクションで指定できます。
[mearm] # define move angle per operation (min, max). base_by = (4, 6) upper_by = (4, 6) lower_by = (4, 6)
その他技術的な解説
トラッキング処理時のフレームの調整
トラッキング処理時に大きい解像度のフレームを処理しようとすると ラズパイではカクツキがでました。 試行錯誤の結果、フレームの解像度は以下のように 320 * 240 (16fps)にしています。
[camera] # deifne frame resolution and frame rate. # (320 * 240 16fps : recommend setting) frame_prop = (320, 240, 16)
前後のアームの動作
アームの前後の動きは、カメラに映る物体の大きさによってコントロールします。
まず基準となるウィンドウサイズ (track_area) を定義します。次にOpenCVが返す物体検知の青枠面積 (track_window_area) を求めます。 最後にそれぞれの面積の平方根の割合(track_area_ratio)を求め、その値に応じて前後にアームを動かします。
参考サイト opencv.blog.jp
Object tracking で camshfit を使う理由
上記と関連しますが、物体検知で使用するアルゴリズムの話です。OpenCVでは、meanshift とそれを拡張した camshift というアルゴリズムがありますが前述の奥行きを判断するためには、camshiftを使います。camshiftでは meanshift で検知した物体のウィンドウサイズを最もフィットするまで更新するため、物体の奥行きの判断に利用できるからです。
https://docs.opencv.org/3.4/db/df8/tutorial_py_meanshift.html
スレッドを使うか否か
サーボモーターにどれくらいコマンドを投げつけて良いのか判断できなかったので、初めはアームの動作はスレッドで並列処理 + wait させることを考えました。 だだ、 0.1秒, 0.05秒くらいのwait間隔でもカクカクした動作になったため、結局はスレッドは使わないことにしました。 スレッドなしだと最大で各フレーム毎にサーボを動作させる処理が投げられることになりますが、可動域を絞っていることもあり、サーボが過度に熱くなることはありませんでした。ジーっていう音はします。
ロックオンでグリップを動かす
MeArmPi には グリップが付いています。せっかくの特徴的な動作なので、検知物体が動かない時間が 2秒経過したらグリップを動かすようにしました。 これは並列処理が必要なので スレッド を使います。
- mearmlib.py
def _grip_mearm(self): self.time_now = time() """update time_delta mearm grip starts wheh time_delta exceed 2(sec) """ self.time_delta = self.time_delta + (self.time_now - self.time_old) if self.time_delta > 2: logger.info( "!! grip close !! time_delta:{}".format(self.time_delta)) if not self.is_test: self.myMeArm.moveToGrip(60) sleep(0.2) logger.info( "!! grip open !! time_delta:{}".format(self.time_delta)) if not self.is_test: self.myMeArm.moveToGrip(30) sleep(0.2) self.time_delta = 0 self.time_old = self.time_now def motion(self, track_window, track_area_ratio, move_ratio, video_prop, margin_window): """mearm "grip"s object locked on""" if move_ratio == (0, 0): if not self.grip_t.is_alive(): self.grip_t = threading.Thread(target=self._grip_mearm) self.grip_t.start() return
まとめ
この分野は初挑戦でしたが、物体追跡できるロボットができたらと思い立ち試行錯誤の末、自分なりのゴールにたどり着きました。
Youtube などで公開されているロボットの物体追跡の動画をみるときびきびとした動作で精度も素晴らしくどういう仕組みなのか知りたいものがたくさんあります。 きっと今回作成した仕組み自体検討外れなものかもしれませんし、もっとスマートな方法があるんだろうなと思います。
それでも問題を解決する過程や、できたときの達成感を考えると有意義な時間でした。
参考
- Video Streaming with Flask
https://blog.miguelgrinberg.com/post/video-streaming-with-flask https://github.com/miguelgrinberg/flask-video-streaming
Netflow Visualization with Logstash netflow module
Netflow Visualization
Logstash netflow module を使うことで、収集、正規化(使いやすい形にする)、可視化まで全部やってくれるのでさらに簡単になりました。
The Logstash Netflow module simplifies the collection, normalization, and visualization of network flow data Netflow module
ダッシュボードイメージ
バージョンは、ELK 5.6 以上が必要になります。
Requirements These instructions assume you have already installed Elastic Stack (Logstash, Elasticsearch, and Kibana) version 5.6 or higher. The products you need are available to download and easy to install.
以前(5.5の時に検証)は設定を理解するのが難しいのと可視化ダッシュボードの作成が必要でした。
環境(components)
components | description |
---|---|
elasticsearch | search engine |
kibana | dashboard |
logstash | netflow collector |
nginx (optional) | reverse proxy (restricting access with HTTP Basic Authentication on kibana) * id/pass: elastic/changeme |
Tested version
- kibana / elasticsearch : 6.3.1 and 6.5.1
- logstash : 6.3.1
- CentOS7
デプロイ
細かい手順は、GitHubに書きました。
Enable logstash netflow module
Logstash で netflow module を有効にするには logstash の起動オプション と logstash.yml を編集します。
- Logstash Netflow Module https://www.elastic.co/guide/en/logstash/current/netflow-module.html
Dockerコンテナとしてデプロイする場合は logstash の 起動シェルにオプションを付与します。 udp.port はデフォルトで 2055 ですがオプションの備忘録的に明示的に記載しています。
./dockerfiles/logstash/run.sh
#!/bin/bash LS_LOG=/var/log/logstash/logstash-plain.log su - logstash -s /bin/bash -c \ "export JAVA_HOME=/etc/alternatives/jre_openjdk;export PATH=$PATH:$HOME/bin:$JAVA_HOME/bin;/usr/share/logstash/bin/logstash --setup -M netflow.var.input.udp.port=2055 --path.settings /etc/logstash" & su - logstash -s /bin/bash -c \ "test -e ${LS_LOG} || touch ${LS_LOG}" tail -f ${LS_LOG}
./dockerfiles/logstash/logstash.yml
path.data: /var/lib/logstash path.logs: /var/log/logstash modules: - name: netflow var.input.udp.port: 2055 var.elasticsearch.hosts: "elasticsearch:9200" var.kibana.host: "kibana:5601"
Google Directory API を使って CLI アプリケーション を作る
What's this
Google CLI Client (googlectl) using Directory API and Cliff
API and Library | Desctiption |
---|---|
G Suite Admin SDK - Directory API | The Directory API lets you perform administrative operations on users, groups, organizational units, and devices in your account. |
Cliff | Command Line Interface Formulation Framework provided by OpenStack Oslo Project |
Function
Command | Description |
---|---|
user list | listing users in your domain |
group list | listing groups in your domain |
group member list | listing members of the group |
user show | show user information details |
group show | show group information details |
Install
How it works
List command example
You can get user list using Google Directory API.
$ googlectl user list +-----------------------------+---------+ | PrimaryEmail | isAdmin | +-----------------------------+---------+ | alice@yourdomain.com | False | | bob@yourdomain.com | True | +-----------------------------+---------+
1. Authentication and Authorization (OAuth 2.0)
Using OAuth 2.0 to Access Google APIs
https://developers.google.com/identity/protocols/OAuth2
Google APIs use the OAuth 2.0 protocol for authentication and authorization. Google supports common OAuth 2.0 scenarios such as those for web server, installed, and client-side applications.
This App (Google APIs) uses OAuth2.0 Client ID
- credentials.py
Get credentails using authorization flow.
ref: https://developers.google.com/admin-sdk/directory/v1/quickstart/python
SCOPES = [ 'https://www.googleapis.com/auth/admin.directory.user', 'https://www.googleapis.com/auth/admin.directory.group', 'https://www.googleapis.com/auth/admin.directory.group.member' ] CLIENT_SECRET_FILE = './client_secret.json' APPLICATION_NAME = 'Google Directory API Python' def get_credentials(): """Gets valid user credentials from storage. If nothing has been stored, or if the stored credentials are invalid, the OAuth2 flow is completed to obtain the new credentials. Returns: Credentials, the obtained credential. """ home_dir = os.path.expanduser('~') credential_dir = os.path.join(home_dir, '.credentials') if not os.path.exists(credential_dir): os.makedirs(credential_dir) credential_path = os.path.join(credential_dir, 'admin-directory_v1-python-quickstart.json') store = Storage(credential_path) credentials = store.get() if not credentials or credentials.invalid: flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES) flow.user_agent = APPLICATION_NAME if flags: credentials = tools.run_flow(flow, store, flags) else: # Needed only for compatibility with Python 2.6 credentials = tools.run(flow, store) print('Storing credentials to ' + credential_path) return credentials
- libgooglectl.py
Initialize the Client Class and set credentials.
class Client(object): def __init__(self): self.credentials = googlectl.credentials.get_credentials() self.http = self.credentials.authorize(httplib2.Http()) self.service = discovery.build( 'admin', 'directory_v1', http=self.http, cache_discovery=False)
2. Get data (G Suite Admin SDK Directory API)
- libgooglectl.py
Get user list using Google Directory API and define colums you want to output ("primaryEmail", "isAdmin")
def list_users(self, number): result = self.service.users().list( customer='my_customer', maxResults=number, orderBy='email').execute() users = result.get('users', []) if not users: return {} users = { user['primaryEmail']: user['isAdmin'] for user in result['users'] } return users
See Directory API Response belows.
Directory API reference : Users: list
https://developers.google.com/admin-sdk/directory/v1/reference/users/list?hl=ja
Request HTTP request
GET https://www.googleapis.com/admin/directory/v1/users
Receive Response
{ "kind": "admin#directory#users", "etag": etag, "users": [ { "primaryEmail": "alice@yourdomain.com", "isAdmin": false, . . }, { "primaryEmail": "bob@yourdomain.com", "isAdmin": true, . . } ] }
3. Return data to be formatted (Cliff)
List Commands
https://docs.openstack.org/cliff/latest/user/list_commands.html
Lister The cliff.lister.Lister base class API extends Command to allow take_action() to return data to be formatted using a user-selectable formatter. Subclasses should provide a take_action() implementation that returns a two member tuple containing a tuple with the names of the columns in the dataset and an iterable that will yield the data to be output.
- list.py
take_action() implementation
class UserList(Lister): def get_parser(self, prog_name): parser = super(UserList, self).get_parser(prog_name) parser = _append_global_args(parser) return parser def take_action(self, parsed_args): client = googlectl.libgooglectl.Client() users = client.list_users(parsed_args.number) return (('PrimaryEmail', 'isAdmin'), ( (primaryemail, users[primaryemail]) for primaryemail in users))
List Output Formatters cliff is delivered with two output formatters for list commands. Lister adds a command line switch to let the user specify the formatter they want, so you don’t have to do any extra work in your application.
4. Command output
Finally you can see formatted data output (The table formatter : default)
$ googlectl user list +-----------------------------+---------+ | PrimaryEmail | isAdmin | +-----------------------------+---------+ | alice@yourdomain.com | False | | bob@yourdomain.com | True | +-----------------------------+---------+
Reference
- OpenStack Osloを使おう - cliff編
https://www.slideshare.net/h-saito/openstack-oslo-cliff
- cliff – Command Line Interface Formulation Framework
https://docs.openstack.org/cliff/latest/index.html
https://developers.google.com/api-client-library/python/guide/aaa_oauth
Raspberry Pi を Netflow Capture として使う
Raspberry Pi を Netflow Capture として使う
前回は、Netflow対応機器がない場合でも Vyosで手軽に Netflow を取得できる話でした。 kodamap.hatenablog.com
今回は Raspberry Pi を使って Netflow をCaptureする方法です。
参考サイト: NetFlow probe using Raspberry
https://blog.pandorafms.org/netflow-probe-using-raspberry/
L2SWの Port Mirroring を有効にして ターゲットポート を Raspberry Pi の 有線ポート(eth0(*))と接続します。
- Raspberry pi で パケットを Netflow Capture フローを生成 , Logstash へ 送る
- Logstash で 解析して Elasticsearch に送る
- Elasticsearch にデータを格納する
- kibana で可視化する
(*) 現状(2017/7現在) Raspberry では 1Gbpsのポートがありません。USBも2.0。 Raspberry pi has no 1Gpbs NIC and USB 2.0 interfaces.
今回使ったもの
Tools | Role |
---|---|
Raspberry Pi Zero W (RASPBIAN JESSIE LITE) | Netflow capture and prober |
Buffalo USB LAN adapter | 1Gpbs NIC (*) |
Netgear GS105E | L2SW (port mirroring enabled) |
Logstash(5.5.0) | Netflow collector |
Elasticsearch(5.5.0) | Search engine |
Kibana(5.5.0) | Virtualizer |
Install and Configure (fprobe)
- install
sudo apt install fprobe sudo apt install nfdump
- configure
sudo vi /etc/default/fprobe #fprobe default configuration file #INTERFACE="wlan0" INTERFACE="eth0" FLOW_COLLECTOR="<Netflow Collector IP>:2055 127.0.0.1:9995" #fprobe can't distinguish IP packet from other (e.g. ARP) OTHER_ARGS="-fip"
- modify /etc/rc.local
起動時にIPアドレス割り当てをflushする
ip addr flush dev eth0
こんな感じになりました。
Kibana + Elasticsearch + Logstash を使って Netflow を可視化する
Kibana + Elasticsearch + logstash を使って Netflow を可視化する
ELK Stack (Kibana + Elasticsearch + logstash) を使って Netflow を可視化する方法のメモです。
- kibana dashboard example
Netflow とは
- 1996年にシスコシステムズによって開発された、通信の流れを収集するためのネットワーク・プロトコル
- パケットの共通属性から通信フローの統計情報を生成(src ip /dst ip , src port /dst port, protocol number, etc..)
パケットキャプチャは、リアルタイムで詳細な情報が取れますが、情報量が多いので定量的な時系列データの収集には不向きです。 SNMPは、時系列にトラフィック量をモニタリングできますが、より詳細なフローの収集はできません。
Netflowは、データの量だけを監視するだけでなく、トラフィックの発信元、発信先、サービスまたはアプリケーションの種類を把握できるので、パケットキャプチャ と SNMP の「いいとこ取り」というのは納得できます。
今回使ったもの
Tools | Role |
---|---|
Vyos(1.1.7) | Netflow compatible device |
Logstash(5.5.0) | Netflow collector |
Elasticsearch(5.5.0) | Search engine |
Kibana(5.5.0) | Virtualizer |
データの流れ
- Vyos で収集した Flow set を Logstash に送る
- Logstash で 解析して Elasticsearch に送る
- Elasticsearch にデータを格納する
- kibana で可視化する
設定
Logstash configuration example
Netflowコレクターとなる Logstashの設定です。 基本的には公式ドキュメント通りに設定して、input, filter, output の設定を追加します。
- netflow codec plugin
https://www.elastic.co/guide/en/logstash/current/plugins-codecs-netflow.html
- How Logstash works
https://www.elastic.co/guide/en/logstash/current/pipeline.html
input
netflow code (plugin) を指定
/etc/logstash/conf.d/netflow.conf
input { udp { port => 2055 codec => netflow { versions => [5, 9] } type => netflow } }
filter
/etc/logstash/conf.d/netflow.conf
Download the GeoIP database
curl -O http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz tar zxvf GeoLite2-City.tar.gz cp -p GeoLite2-City_*/GeoLite2-City.mmdb /etc/logstash/.
GeoIP から 宛先IPの ロケーションを取得
filter { geoip { target => "geoip" source => "[netflow][ipv4_dst_addr]" database => "/etc/logstash/GeoLite2-City.mmdb" } }
output
/etc/logstash/conf.d/netflow.conf
elasticsearch に送る
output { elasticsearch { hosts => ["elasticsearch:9200"] } stdout { codec => rubydebug } }
デプロイ
docker 上にデプロイします。
- build and run
git clone https://github.com/kodamap/kibana-elasticsearch/ cd kibana-elasticsearch docker-compose build docker-compute up -d
- docker ps
Name Command State Ports ------------------------------------------------------------------ elasticsearch /run.sh Up 0.0.0.0:9200->9200/tcp kibana /run.sh Up 0.0.0.0:5601->5601/tcp logstash /run.sh Up 2055/tcp, 0.0.0.0:2055->2055/udp nginx /run.sh Up 0.0.0.0:5681->5681/tcp
Vyos configuration サンプル
Interfaceの着信パケットについてフローが生成されるので双方向のフローを取得するにはすべてのインターフェースで有効化します。
set system flow-accounting interface eth0 set system flow-accounting interface eth1 set system flow-accounting netflow engine-id 100 set system flow-accounting netflow version 9 set system flow-accounting netflow timeout expiry-interval 60 set system flow-accounting netflow server <logstash ip> port 2055 set system flow-accounting netflow sampling-rate 60
まとめ(まとまってない)
ELK のお蔭で手軽にNetflowの可視化ができました。まずは日々のデータを蓄積しようと思います。 次のステップはこれをどう活用するのか(「異常」や「脅威」を検出)ですが、何をもって「異常」とするのか ? 「通常時」のデータと比較して把握することが必要です。
参考サイト
http://www.infraexpert.com/study/netflow1.html
http://komeiy.hatenablog.com/entry/2014/10/24/234353
https://www.elastic.co/guide/en/logstash/current/plugins-codecs-netflow.html
http://enog.jp/wp-content/uploads/2015/09/enog43_elk_0904.pdf
スマホでミニ四駆を動かす
はじめに
昔懐かしミニ四駆です。小学生の頃「ラジコンみたいに操縦出来たら」と思ったことがあります。 最近「改造ミニ四駆製作キット [MKZ4]」という素晴らしいものを発見。これを使うと、ミニ四駆がスマホ(ブラウザ)から操作できるようになります。 さらにMilkCocoaのようなバックエンドサービスと連携させるとインターネット経由でリアルタイムの操作が可能になります。
使ったもの
ワイルドミニ四駆
タミヤ ワイルドミニ四駆シリーズ No.07 キングキャブ Jr. 17007
- 出版社/メーカー: タミヤ(TAMIYA)
- 発売日: 2012/05/05
- メディア: おもちゃ&ホビー
- この商品を含むブログを見る
改造ミニ四駆製作キット [MKZ4] http://cerevo.shop-pro.jp/?pid=104131889
MKZ4用 プログラム書き込みキット「MKZ4WK」 http://cerevo.shop-pro.jp/?pid=104131917
【Cerevo公式】徹底解説!MKZ4ガイドブック Kindle版 (とても詳しく書かれているので超おすすめです。)
- 作者: 株式会社Cerevo
- 出版社/メーカー: 株式会社Cerevo
- 発売日: 2016/07/27
- メディア: Kindle版
- この商品を含むブログを見る
はんだごてセット
Zacro ダイヤル式電子はんだごて セット ハンダゴテ 温度調節可能(200?450℃) 5個交換コテ先付き 60W 110V
- 出版社/メーカー: Zacro
- メディア: その他
- この商品を含むブログを見る
マルチメーター
Crenova デジタルマルチメーター 電圧・電流・周波数・抵抗・導通測定テスター
- 出版社/メーカー: Crenova
- メディア: Automotive
- この商品を含むブログを見る
全体イメージ
インターネット経由で MilkCocoa に データを送信すると、ミニ四駆に搭載した無線LANモジュールに書き込んだプログラムが、それを検知してミニ四駆を動作させます。
この仕組みの面白いところは操作デバイス(スマホ)とミニ四駆が 互いにインターネットに接続して BaaS (MilkCocoa) を通して動作する部分です。スマホはミニ四駆と直接接続する必要はありません。インターネットに接続してさえいれば遠距離からでも操作が可能です。
ミニ四駆の組み立て
「【Cerevo公式】徹底解説!MKZ4ガイドブック」を参考に準備していきます。
- 難関
無線LANモジュール(ESP8266)のはんだ付けはとっても難しい・・。心が折れかける。
チップ抵抗。すごく小さい。
バックエンドサービスの準備(Milkcocoaでアプリ作成)
インターネット経由で操作するために、MilkCocoaでアプリを作成します。また、API Keyを生成して操作クライアント(デバイス)を限定します。
MilkCocoa(https://mlkcca.com/)
- アプリ作成
- データストア
- APIKeyの作成
- 認証済みクライアント以外を接続不可にする
操作される側(ミニ四駆 + ESP2660) の準備
- プログラムの書き込み Milkcocoa(バックエンドサービス)と連携するプログラムをESP2660に書き込みます。 プログラムは、株式会社Cerevoの github リポジトリからダウンロードします。
git clone https://github.com/cerevo/MKZ4
- milkcocoa_esp8266.ino
WLANの設定、MILKCOCOAの設定、API Keyなど設定します。
/************************* WiFi Access Point *********************************/ #define WLAN_SSID "xxxxxxxxxxxx" //SSID名 #define WLAN_PASS "xxxxxxxxxxxx" //SSID パスワード /************************* Your Milkcocoa Setup *********************************/ #define MILKCOCOA_APP_ID "xxxxxx" //milkcocoa idを記載 #define MILKCOCOA_DATASTORE "xxxx" //milkcocoa data store 名を記載 //Milkcocoaで取得したAPI Key Milkcocoa *milkcocoa = Milkcocoa::createWithApiKey(&client, MQTT_SERVER, MILKCOCOA_SERVERPORT, MILKCOCOA_APP_ID, MQTT_CLIENTID, "API_Key", "API_Secret");
操作する側(スマホ、ブラウザ)の準備
Milkcocoaにデータを送信(push)するUIを準備します。
- アプリ設定
js_func.js
Application 名 (xxxxxx部分)、API_Key、API_Secret を設定します。
var milkcocoa = MilkCocoa.connectWithApiKey('xxxxxx.mlkcca.com', "API_Key", "API_Secret");
- ボタンの準備
操作したボタンがわかるように、オリジナルの画像を少々加工します。
スマホでの画面(タッチすると色が変わるようにする。)
- js_func.jsの変更
var command = {"f_left":1, "forward":2, "f_right": 3, "stop":0 , "b_left":7 , "back":8, "b_right":9}; var target = "none" function send_data(n) { var searchvalue = n ; for (var key in command) { if (searchvalue == command[key]){ send(n); document.getElementById(key).src = "./image/" + key + "2.png"; // ボタンを押したときの画像 } else { document.getElementById(key).src = "./image/" + key + ".png"; // 元の画像 } } }
- test1.htmlの変更
<!DOCTYPE html> <html lang="ja"> <head> </script> <script src="http://cdn.mlkcca.com/v2.0.0/milkcocoa.js"></script> <script type="text/javascript" src="./js_func.js"></script> <style> body { background: #319BE4; } </style> </head> <body> <form> <table border=0> <tr><td><img onclick="send_data(1)" src="./image/f_left.png" width="160" height="160" id="f_left" alt="f_left" /></td> <td><img onclick="send_data(2)" src="./image/forward.png" width="160" height="160" id="forward" alt="forward" /></td> <td><img onclick="send_data(3)" src="./image/f_right.png" width="160" height="160" id="f_right" alt="f_right" /></td> </tr> <tr><td></td> <td><img onclick="send_data(0)" src="./image/stop.png" width="160" height="160" id="stop" alt="stop" /></td> <td></td> </tr> <tr><td><img onclick="send_data(7)" src="./image/b_left.png" width="160" height="160" id="b_left" alt="b_left" /></td> <td><img onclick="send_data(8)" src="./image/back.png" width="160" height="160" id="back" alt="back" /></td> <td><img onclick="send_data(9)" src="./image/b_right.png" width="160" height="160" id="b_right" alt="b_right" /></td> </tr> </table> </form> </body> </html>
完成