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 の動き

動画では黄色い物体を追跡しています。

www.youtube.com

もし試しに使われる際は、安全には十分に注意して周りに人がいない場所で飛ばしてください。

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 から ブラウザへ 非同期 に反映されます。

app

  • 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) 。

tracking

技術的な話(悩んだところ)

ラッキング処理のフレーム調整

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)

https://github.com/opencv/opencv/tree/master/samples/python

MeArmPi で Object tracking

MeArmPi で Object tracking

対象物体を検知し追跡するロボットです。ロックオンするとグリップが動きます。

www.youtube.com

コードは GitHub に上げています。

github.com

MeArmPi とは?

MeArmPi は ラズパイで動くロボットアームキットです。ジョイスティックでの操作やブラウザからプログラミングを組んで動かすことができます。

mime.co.uk

MeArmPiには、カメラは付属していないので、カメラモジュール(PiCamera)を取り付けました。

環境

仕組み

ブラウザから見るロボットカメラの画像です。「黄色いもの」を検知/追跡しています。 追跡する物体の位置が赤枠の境界を超えると枠内に戻す方向にロボットアームを動かすという仕組みです。 下は物体の検出を表す画像(ヒストグラムの逆投影)です。物体が白く見えます。

cube

Python でサーボを動かすために

コードは以下を使わせてもらいました。

  • MeaArmPi TechnicalOverview

https://groups.google.com/group/mearm/attach/18a4eb363ddaa/MeArmPiTechnicalOverviewV0-2DRAFT.pdf?part=0.1

実際にやる際は、このドキュメントにある sample コードを実行してサーボが意図した角度に動くかどうか十分に確認することをお勧めします。

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)を求め、その値に応じて前後にアームを動かします。

 track\_area\_ratio = \displaystyle \frac{\sqrt{track\_area}}{\sqrt{track\_window\_area}}

参考サイト 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

www.elastic.co

ダッシュボードイメージ

netflow_dashboard_6x

バージョンは、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の時に検証)は設定を理解するのが難しいのと可視化ダッシュボードの作成が必要でした。

kodamap.hatenablog.com

環境(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に書きました。

github.com

Enable logstash netflow module

Logstash で netflow module を有効にするには logstash の起動オプション と logstash.yml を編集します。

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

GitHub

github.com

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

netflow dashboard

  • 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(*))と接続します。

netflow dashboard

  1. Raspberry pi で パケットを Netflow Capture フローを生成 , Logstash へ 送る
  2. Logstash で 解析して Elasticsearch に送る 
  3. Elasticsearch にデータを格納する
  4. 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

こんな感じになりました。

netflow raspz

Kibana + Elasticsearch + Logstash を使って Netflow を可視化する

Kibana + Elasticsearch + logstash を使って Netflow を可視化する

ELK Stack (Kibana + Elasticsearch + logstash) を使って Netflow を可視化する方法のメモです。

netflow dashboard

Netflow とは

  • 1996年にシスコシステムズによって開発された、通信の流れを収集するためのネットワーク・プロトコル
  • パケットの共通属性から通信フローの統計情報を生成(src ip /dst ip , src port /dst port, protocol number, etc..)

パケットキャプチャは、リアルタイムで詳細な情報が取れますが、情報量が多いので定量的な時系列データの収集には不向きです。 SNMPは、時系列にトラフィック量をモニタリングできますが、より詳細なフローの収集はできません。

Netflowは、データの量だけを監視するだけでなく、トラフィックの発信元、発信先、サービスまたはアプリケーションの種類を把握できるので、パケットキャプチャ と SNMP の「いいとこ取り」というのは納得できます。

www.atmarkit.co.jp

今回使ったもの

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

データの流れ

  1. Vyos で収集した Flow set を Logstash に送る
  2. Logstash で 解析して Elasticsearch に送る 
  3. Elasticsearch にデータを格納する
  4. kibana で可視化する

howitworks

設定

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

タミヤ ワイルドミニ四駆シリーズ No.07 キングキャブ Jr. 17007

改造ミニ四駆製作キット [MKZ4] http://cerevo.shop-pro.jp/?pid=104131889

MKZ4用 プログラム書き込みキット「MKZ4WK」 http://cerevo.shop-pro.jp/?pid=104131917

Cerevo公式】徹底解説!MKZ4ガイドブック Kindle版 (とても詳しく書かれているので超おすすめです。)

【Cerevo公式】徹底解説!MKZ4ガイドブック

【Cerevo公式】徹底解説!MKZ4ガイドブック

はんだごてセット

マルチメーター

全体イメージ

インターネット経由で MilkCocoa に データを送信すると、ミニ四駆に搭載した無線LANモジュールに書き込んだプログラムが、それを検知してミニ四駆を動作させます。

f:id:kodamap:20161007000713p:plain

この仕組みの面白いところは操作デバイス(スマホ)とミニ四駆が 互いにインターネットに接続して BaaS (MilkCocoa) を通して動作する部分です。スマホミニ四駆と直接接続する必要はありません。インターネットに接続してさえいれば遠距離からでも操作が可能です。

ミニ四駆の組み立て

「【Cerevo公式】徹底解説!MKZ4ガイドブック」を参考に準備していきます。

  • 難関

無線LANモジュール(ESP8266)のはんだ付けはとっても難しい・・。心が折れかける。 f:id:kodamap:20170222230413j:plain

チップ抵抗。すごく小さい。 f:id:kodamap:20170222230547j:plain

バックエンドサービスの準備(Milkcocoaでアプリ作成)

インターネット経由で操作するために、MilkCocoaでアプリを作成します。また、API Keyを生成して操作クライアント(デバイス)を限定します。

MilkCocoa(https://mlkcca.com/)

  1. アプリ作成
  2. データストア
  3. APIKeyの作成
  4. 認証済みクライアント以外を接続不可にする

操作される側(ミニ四駆 + ESP2660) の準備

  • プログラムの書き込み Milkcocoa(バックエンドサービス)と連携するプログラムをESP2660に書き込みます。 プログラムは、株式会社Cerevogithub リポジトリからダウンロードします。
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");
  • ボタンの準備

操作したボタンがわかるように、オリジナルの画像を少々加工します。

スマホでの画面(タッチすると色が変わるようにする。) f:id:kodamap:20170222231934j:plain

  • 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>

完成

f:id:kodamap:20170222232504j:plain