22
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-11-07

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

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

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

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を追加することを忘れずに。
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を開きます
②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"
}

⑨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/

22
27
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?