2016/11/07

AWS IoTでM2Mことはじめ(iOS, Node.js)

AWS IoTでMQTTプロトコルを使用したNode.js, iOS(Swift)の疎通サンプルがあります。
2016年11月現在動作確認済み。AWS IoT自体は昨年にリリースされて記事自体も昨年書いたものですが、備忘録として残しておきます。

Amazon IoTってなに?

ハードウェアをサポートするバックエンドの仕組みをAWSに素早く、簡単に構築できるクラウドサービス。インターネットに接続されたデバイスとデバイスをつなぎ、安全な双方向性通信を提供。

プロトコルはMQTT

  • TCP/IP 上で動作するpublish/subscribeモデルに基づく軽量なメッセージプロトコル。
  • 軽量メッセージ配信に特化しており、センサーデータなどに使用される(M2M)、
  • Facebook MessengerもMQTT
  • 256メガバイトが最大
  • 固定長ヘッダーが最小2バイトとオーバーヘッドが少なく、またプロコトルも単純です。そのため、HTTPに比べるとネットワーク帯域および処理速度に優れています。また、処理が少ないということで、消費電力も少なくなっており、モバイル機器にも向いている

キーワード

Topic、QoS、Device gateway, Registry, Thing Shadow, Rules

Getting Started

まずはコンソール上でpub/sub

OSS,MQTT実装のmosquittoクライアントをインストール

npm install mqtt

AWS CLI のアップグレード

IoT Message Broker を操作するには AWS CLI 1.8.12 以上が必要
sudo pip install awscli --upgrade

認証関連

cert.json というファイル名で保存
aws iot create-keys-and-certificate --set-as-active > cert.json
jqコマンドがなければ
brew install jq
各情報を保存
cat cert.json | jq .keyPair.PublicKey -r > thing-public-key.pem
cat cert.json | jq .keyPair.PrivateKey -r > private-key.pem
cat cert.json | jq .certificatePem -r > cert.pem
ルートCAをシマンテックサイトから取得
curl -o rootCA.pem https://www.symantec.com/content/en/us/enterprise/verisign/roots/VeriSign-Class%203-Public-Primary-Certification-Authority-G5.pem

IAM Roleの設定

MQTT ブローカーに pub/sub するための IAM Role を Certification に設定
iot サービスを操作できる "PubSubToAnyTopic" という名前のポリシーを作成
以下jsonファイルを作成
policy.json
{
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Action":["iot:*"],
        "Resource": ["*"]
    }]
}

ポリシーをプリンシパルにひも付けます。 プリンシパルとなるのは aws iot create-keys-and-certificateコマンドを実行した時の certificateArn です
aws iot attach-principal-policy --principal "arn:aws:iot:ap-northeast-1:1111111:cert/SNIP" --policy-name "PubSubToAnyTopic"

準備完了

pub/sub通信が出来る状態となりました。

MQTT エンドポイントの確認

aws iot describe-endpointでAWSアカウントごとに異なるエンドポイントの確認
$ aws iot describe-endpoint
{
    "endpointAddress": "HOGE.iot. northeast-1.amazonaws.com"
}

Subscribe

mosquitto_sub --cafile rootCA.pem --cert cert.pem --key private-key.pem -h "HOGE.iot.ap-northeast-1.amazonaws.com" -p 8883 -q 1 -d -t topic/test -i clientid1
Client clientid1 sending CONNECT
Client clientid1 received CONNACK
Client clientid1 sending SUBSCRIBE (Mid: 1, Topic: topic/test, QoS: 1)
Client clientid1 received SUBACK
Subscribed (mid: 1): 1

Publish

mosquitto_pub --cafile rootCA.pem --cert cert.pem --key private-key.pem -h "HOGE.iot.ap-northeast-1.amazonaws.com" -p 8883 -q 1 -d -t topic/test -i clientid2 -m "Hello, World"
Client clientid2 sending CONNECT
Client clientid2 received CONNACK
Client clientid2 sending PUBLISH (d0, q1, r0, m1, 'topic/test', ... (12 bytes))
Client clientid2 received PUBACK (Mid: 1)
Client clientid2 sending DISCONNECT

確認

Subscribe側でHello, Worldの確認
Client clientid1 received PUBLISH (d0, q1, r0, m1, 'topic/test', ... (12 bytes))
Client clientid1 sending PUBACK (Mid: 1)
Hello, World

とpublishした後、メッセージがHello, Worldと表示されていればOKです。

Node.jsでやってみる

AWS IoT SDK for JavaScriptのインストール
npm install aws-iot-device-sdk
先ほどのmqtt.jsのラッパーでクライアントインスタンスを経由して、
デバイスとAWS IoTをセキュアに接続します。mqttを意識することなく使えます。
deviceクラスとthingShadowクラスがあります。

Subscribe

deviceSub.js
var awsIot = require('aws-iot-device-sdk');
var fs = require('fs');

// 先ほど生成した認証ファイルを指定
var KEY = __dirname + '/private-key.pem';
var CERT = __dirname + '/cert.pem';
var TRUSTED_CA = __dirname + '/rootCA.pem';

var device = awsIot.device(
{
   keyPath: KEY,
  certPath: CERT,
    caPath: TRUSTED_CA,
  clientId: 'clientid2',
    region: 'ap-northeast-1'
});


device
  .on('connect', function() {
    console.log('connect');
    device.subscribe('topic_1', {'qos': 1});
    });

device
  .on('message', function(topic, payload) {
    console.log('message', topic, payload.toString());
  });

Publish

devicePub.js
var awsIot = require('aws-iot-device-sdk');
var fs = require('fs');

// 先ほど生成した認証ファイルを指定
var KEY = __dirname + '/private-key.pem';
var CERT = __dirname + '/cert.pem';
var TRUSTED_CA = __dirname + '/rootCA.pem';

var device = awsIot.device(
{
   keyPath: KEY,
  certPath: CERT,
    caPath: TRUSTED_CA,
  clientId: 'clientid2',
    region: 'ap-northeast-1'
});

device
  .on('connect', function() {
    console.log('connect');
    device.publish('topic_1', 'Hello mqtt\n\n', {'qos': 1});
});
node deviceSub.jsを立ち上げて、
別画面で
node devicePub.jsを打つと、
Sub側で
connect
message topic_1 Hello mqtt
とmessegeが表示されていればOK

iOSでやってみる

流れとしては、先ほど作成した証明書3つのプロジェクト内にコピーし、
クライアントを利用して指定したトピックにpublishするものです。
今回の例は、GPS情報(json)を送信と、ボタンアクションで文字列の送信を行います。

iOS向けMQTTクライアント

Swiftで書かれたMQTTクライアントMoscapsule
https://github.com/flightonary/Moscapsule
こちらを使うと X.509 形式の証明書を利用した MQTT 通信が行えます。OpenSSL-Universalも依存しているので、こちらも一緒にインポート。
use_frameworks!

target 'MQTTSample' do
  pod 'Moscapsule', :git => 'https://github.com/flightonary/Moscapsule.git'
  pod 'OpenSSL-Universal', '~> 1.0.1.l'
  pod 'SwiftyJSON'
end

証明書のインポート

Xcodeプロジェクトに先ほど用意した証明書、Privateキーファイル、ルートCAファイルをインポートします。

iOSからTopicに向けてPublish

位置情報を利用するため、Info.plistNSLocationWhenInUseUsageDescriptionを追加することを忘れずに。
ViewController.swift
import UIKit
import Moscapsule
import SwiftyJSON
import CoreLocation

class ViewController: UIViewController, CLLocationManagerDelegate {

    var manager: CLLocationManager?
    var mqttClient: MQTTClient?

    @IBOutlet weak var latLb: UILabel!
    @IBOutlet weak var lngLb: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        initMQTTClient()

        manager = CLLocationManager()
        manager?.delegate = self

        // 「アプリ使用時のみ許可」でなかったら、ダイアログを出す。
        if CLLocationManager.authorizationStatus() != CLAuthorizationStatus.AuthorizedWhenInUse {
            manager?.requestWhenInUseAuthorization()
        }

        manager?.startUpdatingLocation()
    }

    func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
        if status == .AuthorizedWhenInUse {
            manager.startUpdatingLocation()
        }
    }

    // 位置情報の更新で MQTT のトピックに送信
    func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        let location = locations.first!
        let json: JSON = [
            "state": [
                "reported": [
                    "location": [
                        "latitude": location.coordinate.latitude,
                        "longitude": location.coordinate.longitude
                    ]
                ]
            ]
        ]


        latLb.text = location.coordinate.latitude.description
        lngLb.text = location.coordinate.longitude.description

        publishTopic(json)
    }

    // MQTT クライアントの初期設定
    func initMQTTClient() {
        moscapsule_init()
        let mqttConfig = MQTTConfig(clientId: "server_cert_test",
            host: "HOGE.iot.ap-northeast-1.amazonaws.com", port: 8883, keepAlive: 60)
        let certFile = NSBundle.mainBundle().pathForResource("cert.pem", ofType: "crt")
        let keyFile = NSBundle.mainBundle().pathForResource("private-key.pem", ofType: "key")
        let caFile = NSBundle.mainBundle().pathForResource("rootCA", ofType: "pem")
        mqttConfig.mqttServerCert = MQTTServerCert(cafile: caFile, capath: nil)
        mqttConfig.mqttClientCert = MQTTClientCert(certfile: certFile!, keyfile: keyFile!, keyfile_passwd: nil)
        self.mqttClient = MQTT.newConnection(mqttConfig)
    }

    // トピックに向けた Publish
    func publishTopic(json: JSON) {
        let data = try! json.rawData()
        self.mqttClient!.publish(data, topic: "topic_1", qos: 1, retain: false)
         // device shadowの場合、別途後述
//        self.mqttClient!.publish(data, topic: "$aws/things/iPhone-6/shadow/update", qos: 1, retain: false)
    }

    @IBAction func pushPublish(sender: AnyObject) {
        self.mqttClient!.publishString("iOSからのpublish!!!", topic: "topic_1", qos: 1, retain: false)
    }

    func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
        print("Failure")
    }

}

Subscribe

こちらはさきほどの
node deviceSub.js
で待ち受けていればOK

結果

iOSアプリから位置情報が更新されたら

message topic_1 
{
"state": {
      "reported": {
        "location": {
          "longitude": 135.7848895,
          "latitude": 35.0115832
        }
      }
    }
}   
pushPublishボタンアクションで
message topic_1 iOSからのpublish!!!
上記それぞれ表示されればOK

ここまではただのPub/Sub

ここまではただのMQTTブローカーです。

ここからが真骨頂 Thing Shadows

デバイスのことをThingと呼びます。
Thing Shadow(デバイスの影像)はThingの状態がAWS上に存在するもの
デバイスはオフラインになったり、状態が変更されるケースがあるので、アプリから管理するためにThing Shadowが存在する。永続的な仮想バージョンを作成できる。
retainが不要なのは、Thing Shadowがあるから。
retainというのは最後にPublishされたメッセージをMQTTサーバーが保持しておき、新しいSubscriberにそのメッセージを渡す機能
ThingとThing Shadowは一対一で。Thingに変化があれば、Thing Shadowにも変更が通知される。
reportedがThings、desiredがThing Shadowsの状態を示す

Thing作成

AWS CLIからも可能ですが、
①AWSコンソールからAWS IoTを開きます
②Resources横の Create a Resource を選択
③パネルが展開されてCreate Thingを選択、Nameを入力後、Create
④下のthings一覧から先ほど作成したthingを選択,右サイドパネルが展開されて、
Connect a deviceを選択
⑤Connect a deviceの画面でサポートするSDKの一覧から NodeJS を選択
⑥ Generate certificate and policy を選択
  • Download public key
  • Download private key
  • Download certificate
上記3つのファイルをダウンロード
⑦ Confirm & Start Connecting を選択
⑧以下のようなJSONが表示されるので。コピー
{
    "host": "HOGE.iot.ap-northeast-1.amazonaws.com",
    "port": 8883,
    "clientId": "hoge",
    "thingName": "hoge",
    "caCert": "root-CA.crt",
    "clientCert": "dca2683d89-certificate.pem.crt",
    "privateKey": "dca2683d89-private.pem.key"
}
⑩ダウンロードした3つのファイル、JSON、root-CA.crtを同階層に置きます。
これで準備で完了

AWS CLIでThing Shadow

更新

デバイス iPhone-6 の状態を初期登録
aws iot-data update-thing-shadowコマンドで状態の更新
ルート階層のstateは固定で、その下のreported以下に自由に要素を定義することができる。
$ aws iot-data update-thing-shadow --thing-name iPhone-6  --payload '{"state": {"reported" : {"power" : "off"}}}'   outfile.json
レスポンスにはstateに加えてリクエストのバージョンとリクエストのタイムスタンプなどが付与されます。
outputfile.json
{
  "state": {
    "reported": {
      "power": "off"
    }
  },
  "metadata": {
    "reported": {
      "power": {
        "timestamp": 1450529567
      }
    }
  },
  "version": 1,
  "timestamp": 1450529567
}
パワーオンで更新
$ aws iot-data update-thing-shadow --thing-name iPhone-6  --payload '{"state": {"desired" : {"power" : "on"}}}'   outfile2.json
outputfile2.json
{
  "state": {
    "desired": {
      "power": "on"
    }
  },
  "metadata": {
    "desired": {
      "power": {
        "timestamp": 1450529706
      }
    }
  },
  "version": 2,
  "timestamp": 1450529706
}

desiredがpower onになっています。
状態情報の確認 aws iot-data get-thing-shadowコマンドにて
$ aws iot-data get-thing-shadow --thing-name iPhone-6 outfile3.json
outputfile3.json
{
  "state": {
    "desired": {
      "power": "on"
    },
    "reported": {
      "power": "off"
    },
    "delta": {
      "power": "on"
    }
  },
  "metadata": {
    "desired": {
      "power": {
        "timestamp": 1450529706
      }
    },
    "reported": {
      "power": {
        "timestamp": 1450529567
      }
    }
  },
  "version": 2,
  "timestamp": 1450529787
}
desired(thing shadow), reported(device)の状態で、
deltaはstate.deltaにはデバイスの状態と望ましい状態の差分が抽出される。
デバイス側でのパワーオン処理をしたと仮定して、パワーオンにしてみる。
$ aws iot-data update-thing-shadow --thing-name iPhone-6 --payload '{"state": {"reported" : {"power" : "on"}}}' outfile3.json
再度状態情報を確認
$ aws iot-data get-thing-shadow --thing-name iPhone-6 outfile4.json
outfile4.json
{
  "state": {
    "desired": {
      "power": "on"
    },
    "reported": {
      "power": "on"
    }
  },
  "metadata": {
    "desired": {
      "power": {
        "timestamp": 1450529706
      }
    },
    "reported": {
      "power": {
        "timestamp": 1450529951
      }
    }
  },
  "version": 3,
  "timestamp": 1450529989
}
deltaが無くなり、差分が無くなっていることがわかる。
versionは3となっているが、更新リクエストによってAWS側で自動でインクリメントされる。
これは古いバージョンへ先祖還りを防いだり、他のpublisherによる更新を検知する手段となりうる。
$ aws iot-data update-thing-shadow --thing-name iPhone-6 --payload '{"state": {"reported" : {"power" : "on"}},"version" : 2}' outfile.json

A client error (ConflictException) occurred when calling the UpdateThingShadow operation: Version conflict
更新リクエストに"version" : 2の指定を入れたところ、既にバージョン3になっているため更新が拒否される
現在のバージョンの確認 jqで抜き出し
$ aws iot-data get-thing-shadow --thing-name iPhone-6 outfile4.json | cat outfile4.json | jq .version 
3が出力される。

ルールの作成

①Create a rule、ルールの登録
Rules Engineを通して必要なデータのみをフィルタリングしてデータのやり取りが可能
queryをこのように
SELECT * FROM 'topic_1'
記述して

topic_1にトピックを送信→S3にストアという流れを作ります。
今回はS3にデータを送りたいので「Store the message in a file and store in the cloud (S3)」を選択して、Bucket等の設定を行います。
iphone6-location-${timestamp()}
で保存して、順次GPSデータが保存されていきます。
${timestamp()}なしであれば、上書き保存します。
正しくデータが送信されれば「iphone6」というファイルができているはず
AWS IoTを接続してS3にデータを送る一連の流れの説明

おわりに

AWS IoTでM2Mことはじめはこれで終わりです。
実装のポイントとしては
・ひとまず疎通確認のためにはS3への登録を選んでおいて、疎通確認後に本当に渡したいサービスを登録する
・AWS IoTはQoS(Quality of Service)レベルの2が選択できないので複数投げられてもデータが重複しないような冪等性を確保した設計にするとよい
・連続したデータはTopicにMQTTデータを投げる形で実装し、単発データや選択データ(ランプがつく/消える、や電話をかける、等)はSHADOWSで管理したほうがよい
となります。入力部、出力部のデバイスの実装はなるべくシンプルにし、難しい処理はAWS内で片付けたほうが変更に強い実装ができます。例えばデータの丸め等はデバイス部では行わず、AWS IoTからLambda等を繋いでそこで行ったほうがよいです
(http://dev.classmethod.jp/cloud/aws/cm-advent-calendar-2015-getting-started-again-iot/)
things shadowの状態を監視して、他のデバイスを動かすという展開が考えられます。
http://dev.classmethod.jp/cloud/update-device-shadow-by-lambda/

参考

MQTTについてのまとめ
http://tdoc.info/blog/2014/01/27/mqtt.html
AWS IoT Message BrokerのMQTTでpub/subをやってみた #reinvent
http://dev.classmethod.jp/cloud/aws/pub-sub-with-aws-iot-over-mqtt/
AWS IoTとRuby製MQTTクライアントでPub/Subしてみた
http://qiita.com/hiroeorz@github/items/f933ad1158a08506922a
The MQTT client for Node.js and the browser
https://github.com/mqttjs/MQTT.js
AWS IoT の Device Shadow を iOS アプリから MQTT で使ってみた #reinvent
http://dev.classmethod.jp/cloud/aws/aws-iot-mqtt/
AWS IoTおよびThing Shadowsに関する雑感
http://tdoc.info/blog/2015/10/09/thing_shadows.html
AWS IoTのThing Shadowsを図と実行例で理解する #reinvent
http://dev.classmethod.jp/cloud/aws-iot-things-shadow/
これからAWSを使ってIoTをやってみたい人が抑えておくべき10のキーサービス & 7つのキーワード #reinvent
http://dev.classmethod.jp/cloud/aws/aws-key-service-people-wanted-to-do-iot-should-study/
AWS IoTのいろいろなルールを見てみる&ちょっと試してみる #reinvent
http://dev.classmethod.jp/cloud/aws-iot-rules/
【新機能】AWS IoT のRules EngineがAmazon machine Learningをサポート。IoTと機械学習が一体に
http://dev.classmethod.jp/cloud/aws/aws-iot-supports-integration-with-amazon-machine-learning/