AWS IoTでM2Mことはじめ（iOS, Node.js）

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

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


-  MQTT v3.1.1
-  QoS 0, 1に対応（2には対応していない）
-  Will, Retainに対応していない
→Thing Shadowという状態を検知する仕組みがあるので不要ということ。
-  Payloadサイズ128KB
-  TLS対応
-  WebSocket、WebSecureSocket対応あり（2016/1/28のアップデートにて）
https://aws.amazon.com/jp/about-aws/whats-new/2016/01/aws-iot-now-supports-websockets-custom-keepalive-intervals-and-enhanced-console/
-  認証, 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`

###認証関連
こちらに沿って
http://docs.aws.amazon.com/iot/latest/developerguide/secure-communication.html

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クラスがあります。

詳細はこちら
https://github.com/aws/aws-iot-device-sdk-js

###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も依存しているので、こちらも一緒にインポート。


```Podfile
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.plist`に`NSLocationWhenInUseUsageDescription`を追加することを忘れずに。
Swift3.0にて

```ViewController.swift
import UIKit
import Moscapsule
import SwiftyJSON
import CoreLocation

class ViewController: UIViewController, CLLocationManagerDelegate {
    
    var manager: CLLocationManager?
    var mqttClient: MQTTClient?
    var timer:Timer?
    
    @IBOutlet weak var latLb: UILabel!
    @IBOutlet weak var lngLb: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        initMQTTClient()
        
        manager = CLLocationManager()
        manager?.delegate = self
        // 最高精度
        manager?.desiredAccuracy = kCLLocationAccuracyBestForNavigation
        manager?.pausesLocationUpdatesAutomatically = false
        manager?.allowsBackgroundLocationUpdates = true
        
        // 「アプリ使用時のみ許可」でなかったら、ダイアログを出す。
        if CLLocationManager.authorizationStatus() != CLAuthorizationStatus.authorizedAlways {
            manager?.requestAlwaysAuthorization()
        }
        
        manager?.startUpdatingLocation()
    }

    
    func getGpsData() {
        
        let location = manager?.location
        let coordinate = location!.coordinate
        let lat = coordinate.latitude.description
        let long = coordinate.longitude.description
        
        print("緯度:\(lat) 経度:\(long)")
        
        let json: JSON = [
            "state": [
                "reported": [
                    "location": [
                        "latitude": coordinate.latitude,
                        "longitude": coordinate.longitude
                    ]
                ]
            ]
        ]
        
        publishTopic(json)
    }
    
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization 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
        
        print("緯度:\(location.coordinate.latitude.description) 経度:\(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 = Bundle.main.path(forResource: "cert.pem", ofType: "crt")
        let keyFile = Bundle.main.path(forResource: "private-key.pem", ofType: "key")
        let caFile = Bundle.main.path(forResource: "rootCA", ofType: "pem")
        mqttConfig.mqttServerCert = MQTTServerCert(cafile: caFile, capath: nil)
        mqttConfig.mqttClientCert = MQTTClientCert(certfile: certFile!, keyfile: keyFile!, keyfile_passwd: nil)
        mqttClient = MQTT.newConnection(mqttConfig)
    }
    
    // Device Shadow トピックに向けた Publish
    func publishTopic(_ json: JSON) {
        let data = try! json.rawData()
        guard let mqttClient = mqttClient else { return }
        mqttClient.publish(data, topic: "topic_1", qos: 1, retain: false)
        // device shadowの場合、別途後述
        // mqttClient.publish(data, topic: "$aws/things/iPhone-6/shadow/update", qos: 1, retain: false)
    }
    
    @IBAction func pushPublish(_ sender: AnyObject) {
        guard let mqttClient = mqttClient else { return }
        mqttClient.publish(string: "iOSからのpublish!!!", topic: "topic_1", qos: 1, retain: false)
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        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](https://ap-northeast-1.console.aws.amazon.com/iot/home?region=ap-northeast-1#/dashboard)を開きます
②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が表示されるので。コピー

```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"
}
```

⑨root-CA.crtというファイルをこちらのSymantecのページから取得
https://www.symantec.com/content/en/us/enterprise/verisign/roots/VeriSign-Class%203-Public-Primary-Certification-Authority-G5.pem

⑩ダウンロードした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

Message Broker for AWS IoT (Beta)
http://docs.aws.amazon.com/iot/latest/developerguide/iot-message-broker.html

AWS IoT SDK for JavaScript
https://github.com/aws/aws-iot-device-sdk-js

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/