aws-iot

AWS-IoT ShadowからLEDのオン・オフやDCモーターを回す

More than 1 year has passed since last update.

はじめに

この記事は、JAWS-UG関西IoT専門支部 ✕ 関西おうちハック「夏休み自由研究お助けIoTハンズオン 親子もお1人様も大歓迎!」のハンズオン用資料です。

[1]Raspberry Piに最新のNode.jsをインストールする

Rasbian Jessieの最新バージョン(2016年5月末現在)では、Node-Redを動作させるためか、かなり古いバージョンのnode v.0.10が入っています。これを削除してから新しいnode(v.6.3.0など)をインストールします。

[1.1] node v0.10をアンインストールします

$ sudo apt-get autoremove nodejs

[1.2] nvmをインストールするディレクトリ(/usr/local/nvm)を作成し属性を777に変更しておきます。

$ sudo mkdir /usr/local/nvm
$ sudo chmod 777 /usr/local/nvm

[1.3] nvm.gitリポジトリのクローンを作成し、nvmを使えるようにします。

$ git clone https://github.com/creationix/nvm.git /usr/local/nvm
$ source /usr/local/nvm/nvm.sh

[1.4] nodeを使えるようにします。

$ nvm ls-remote #インストールできるnodeのバージョン一覧を表示・確認
$ nvm install v6.3.0 #v6.3.0をインストール

[1.5] node、npm のバージョンを表示して動作することを確認します。

$ node -v
v6.3.0
$ npm -v
3.10.3

[1.6] ログイン時にnvm、nodeの起動設定を行うようにします
/etc/profile.d/ディレクトリ配下に次の内容のnvm.shを作成します。

$ sudo vi /etc/profile.d/nvm.sh

[1.7] 次の行を記述して保存

/etc/profile.d/nvm.sh
source /usr/local/nvm/nvm.sh

[1.9] node-v6.3.0 が node コマンドで起動するようにdefaultエイリアスを設定します。

$ nvm alias default  v6.3.0

[1.10] GPIOを制御できるようにsudo node コマンドが動くようにします。sudoの動作環境に現在のユーザのコマンド・パスを引き渡す設定です。

$ sudo visudo

次の行をコメントアウトします。

/etc/sudoers
#Defaults       secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

次の行を追加します。

Defaults    env_keep += "PATH"

これで、最新のnodeでGPIOを利用できるようにできました。

[2] Johnney-fiveライブラリをインストールする

[2.1] 次にGPIOをNode.jsでGPIOを扱いやすくするライブラリJohnney-fiveとJohnney-five上でRaspberry piを利用できるようにするraspi-ioライブラリをインストールします。
作業(プロジェクト)ディレクトリをaws-iotとして、そこでライブラリをインストールします。

$ mkdir aws-iot
$ cd aws-iot
$ npm update #インストールする前にアップデートを行っておく
$ npm install johnny-five
$ npm install raspi-io

※インストール中にWARNメッセージが出ますが特に問題ありません。大丈夫です。

[2.2] 以下のサンプルソースblink.jsをviなどで作成します。GPIOの11番ピンに接続したLEDを点滅させるLチカのサンプルです。

blink.js
var five = require("johnny-five");
var Raspi = require("raspi-io");
var board = new five.Board({
  io: new Raspi() 
});       

board.on("ready", function() {
  var led = new five.Led("P1-11");
  led.blink();
});

[2.3] sudo nodeコマンドで実行します。

$ sudo node blink.js
…(中略:たくさんログがでますが大丈夫です)…
1469644879938 Device(s) RaspberryPi-IO  
1469644879981 Connected RaspberryPi-IO  
1469644879993 Repl Initialized  
>> 

LEDが点滅すればOKです。
停止するにはCtrl+Cキーを2回打ちます。

[3]AWS-IoTのThings Shadowでラズパイにプッシュを行う

AWS-IoTでThingsを作成し、Shadow画面からLEDのオン・オフのための制御データをラズパイ側に送信します。ShadowはThingが物理的に接続されていなくても送信動作を行うことができ、後ほど物理的に接続された際にプッシュ通信を行います。

まずAWSマネジメントコンソールからAWS IoTの管理画面にアクセスします。
Kobito.UjO87j.png

[Get started]ボタンをクリックして始めます。
Kobito.EsQmhx.png

[Create Things]ボタンが選択された状態になり、thingの作成のため、[Name]欄に任意の名前を入れて、[Create]ボタンをクリックします。ここでは「MyNewThing」としました。
Kobito.8HDeWC.png
thingが作成され表示されています。
[View Thing]ボタンをクリックします。REST API endpointなどの詳細が表示されます。
AWS_IoT01.png

[3.1]キーペア(公開鍵、秘密鍵)のダウンロード及び証明書のダウンロード

AWS IoTでは通信を行う際にTLSによる暗号化を行って通信を行います。
TLS通信の際に必要な証明書やキーペアのダウンロードを行います。
先ほど登録したMyNewThingの詳細を表示している画面の右下の[Connect a device]というボタンを押下します。
Kobito.vE8A90.png

次の画面ではthingとAWS IoTとの通信は何を使って行うかの確認画面が表示されます。今回はNode.jsのDeviceSDKを使うので[Node.js]をチェックします。[Generate certificate and policy]というボタンが表示されるので選択し、証明書とポリシーを作成します。
しばらく待つと以下の画面になります。
Kobito.47ECql.png

3つのリンクが現れます。

  • 公開鍵のダウンロード
  • 秘密鍵のダウンロード
  • 証明書のダウンロード

各リンクをクリックし、作業PC上に保存しておきます。

  • xxxxx-certificate.pem.crt->証明書
  • xxxxx-private.pem.key->秘密鍵
  • xxxxx-public.pem.key->公開鍵

※証明書はいつでも取得できるようですが、公開鍵、秘密鍵はこのページのみしかダウンロードできないようなので忘れずにダウンロードしておきます。

作業PCにダウンロードした3つのファイルをラズベリーパイの作業ディレクトリ「aws-iot」にアップロードしておきます。
(私はCyberDuckを使いました)

[Comfirm & start connecting]ボタンをタップします。

AWS_IoT02.png

thingの接続情報が表示されますので、作業PCにコピーしておきます。
[Return to Thing Detail]ボタンをタップしResources画面に戻ります。PolicyとCertificationが一覧にリストアップされています。
AWS_IoT03.png

次に、向かって右上のthingの詳細画面にある[Update Shadow]をクリックします。
Kobito.akIXS9.png

Shadow stateのJSONの値を以下のように変えて、[Update Shadow]ボタンをクリックします。

{
  "desired": {
    "flash": 0
  },
  "reported": {
    "flash": 0
  }
}

これでShadowが作成・更新されました。
設定が成功すると以下のようにShadow stateが更新されます。
Kobito.p8oFqk.png
ラズパイ側で、通知を受け取るようにしましょう。

[3.2]ルート証明書をダウンロードする

SSL/TLS通信ではサーバーから取得したデジタル証明書が正当なものであるか確認する必要があります。幾つかの認証局はルート証明書としてブラウザなどに最初から登録されているため、我々が通常見るようなサイトを閲覧する際にわざわざルート証明書をダウンロードする必要はありません。ただし、今回は通信をするのがブラウザではないため、対象のルート証明書だけ予めダウンロードする必要があります。
ラズベリーパイのコンソール画面にて以下のコマンドでルート証明書をダウンロードをします。

# root-CA.crtというファイル名でルート証明書を取得
$ wget -O root-CA.crt https://www.symantec.com/content/en/us/enterprise/verisign/roots/VeriSign-Class%203-Public-Primary-Certification-Authority-G5.pem

[3.3]AWS IoT Device SDKのインストール

Raspberry Pi にaws Iot devise SDK をインストールします。

$ npm install aws-iot-device-sdk

次に、Shadowからの更新データを受け取ってLEDに反映させるサンプルソースshadowJ5Led.js をaws-iotディレクトリ内に作成します。

shadowJ5Led.js
var awsIot = require('aws-iot-device-sdk');
//J5の設定
var five = require("johnny-five");
var Raspi = require("raspi-io");
var board = new five.Board({
  io: new Raspi()
});
var led;
board.on("ready", function() {
  led = new five.Led("P1-11");//11番ピン
});
//Shadowの設定---このJSONを自分のものに差し替える
var thingShadows = awsIot.thingShadow({
        "host": "foo.bar.amazonaws.com",
        "port": 8883,
        "clientId": "MyNewThing",
        "thingName": "MyNewThing",
        "caCert": "root-CA.crt",
        "clientCert": "**********-certificate.pem.crt",
        "privateKey": "**********-private.pem.key"
});

var clientTokenGet, clientTokenUpdate;
var shadowName = 'MyNewThing';

thingShadows.on('connect', function() {
    thingShadows.register( shadowName );
    setTimeout( function() {
       clientTokenGet = thingShadows.get( shadowName );
    }, 2000 );
});

thingShadows.on('status', function(thingName, stat, clientToken, stateObject) {
    console.log('received '+stat+' on '+thingName+': '+
                 JSON.stringify(stateObject));
});

thingShadows.on('delta', function(thingName, stateObject) {
    // LED ON/Off
    var state = stateObject.state.flash;
    console.log("state", state);
    if( parseInt(state,10) == 1 ){
        led.on();
    }else{
        led.off();
    }
    console.log('received delta '+' on '+thingName+': '+
                JSON.stringify(stateObject));
    clientTokenUpdate = thingShadows.update( shadowName, {"state":{"reported": {"flash": state}}});
});

thingShadows.on('timeout', function(thingName, clientToken) {
     console.log('received timeout '+' on '+operation+': '+
                 clientToken);
});
//終了時の処理
board.on("exit", function() {
    led.off();
});

コメント行「//Shadowの設定」以下にあるthingShadows変数の引数にあるJSONは、先ほど作業PCにコピーしておいたthingの接続情報をペーストします。
AWS_IoT02.png

サンプルソースshadowJ5Led.jsを入力し終えたら、以下のコマンドで実行してみましょう。

$ sudo node shadowJ5Led.js

1469651618861 Device(s) RaspberryPi-IO  
1469651618961 Connected RaspberryPi-IO  
1469651618972 Repl Initialized  
>> received accepted on MyNewThing: {"state":{"desired":{"flash":0},"reported":{"flash":0}},"metadata":{"desired":{"flash":{"timestamp":1469653404}},"reported":{"flash":{"timestamp":1469653404}}},"timestamp":1469653434}

うまく接続されていれば上記のようなShadow StateのJSON情報が表示されます。
次に、Shadow Stateを更新して通知されるか確認します。

ブラウザから[Update shadow]ボタンをクリックし、desiredキー内のflashキーの値を1にして、[Update shadow]ボタンをクリックします。
Kobito.8bNLoR.png
すると、ラズパイ側で実行しているshadowJ5Led.jsの表示が以下のように更新されます。

$ sudo node shadowJ5Led.js

1469651618861 Device(s) RaspberryPi-IO  
1469651618961 Connected RaspberryPi-IO  
1469651618972 Repl Initialized  
>> received accepted on MyNewThing: {"state":{"desired":{"flash":0},"reported":{"flash":0}},"metadata":{"desired":{"flash":{"timestamp":1469653404}},"reported":{"flash":{"timestamp":1469653404}}},"timestamp":1469653434}
state 1 ←ここから更新されたShadow stateの表示
received delta  on MyNewThing: {"timestamp":1469655266,"state":{"flash":1},"metadata":{"flash":{"timestamp":1469655266}}}
received accepted on MyNewThing: {"state":{"reported":{"flash":1}},"metadata":{"reported":{"flash":{"timestamp":1469655266}}},"timestamp":1469655266}

この際に、flashキーの値が1となったためLEDが点灯します。
再度Shadow stateで、desiredキー内のflashキーの値を0にして、[Update shadow]ボタンをクリックすると、flashキーの値が0となりLEDが消灯します。

[4] 1つのThings ShadowでモーターとLEDの制御を行う

最後に今までLEDのオン・オフに使ってきたShadowにモーターの強弱を同時に受け取るサンプルプログラムを紹介します。
Shadow stateとして以下のJSONを送信してみてください。

{
  "desired": {
    "flash": { "led": 1, "pwm": 100 }
  },
  "reported": {
    "flash": 0
  }
}
shadowJ5LedMotor.js
var awsIot = require('aws-iot-device-sdk');
//J5の設定
var five = require("johnny-five");
var Raspi = require("raspi-io");
var PWM = require('raspi-pwm').PWM;
var board = new five.Board({
  io: new Raspi()
});
var led, ledVal;
var pwm, pwmVal;
board.on("ready", function() {
  led = new five.Led("P1-11");//11番ピン
  pwm = new PWM("P1-12");
  this.pinMode("P1-13", five.Pin.OUTPUT);
  this.pinMode("P1-15", five.Pin.OUTPUT);

  ledVal = 0;
  pwmVal = 0;

  led.off();
  pwm.write(0);
  this.digitalWrite("P1-13", 1);
  this.digitalWrite("P1-15", 0);
});
//Shadowの設定---このJSONを自分のものに差し替える
var thingShadows = awsIot.thingShadow({
        "host": "foo.bar.amazonaws.com",
        "port": 8883,
        "clientId": "MyNewThing",
        "thingName": "MyNewThing",
        "caCert": "root-CA.crt",
        "clientCert": "**********-certificate.pem.crt",
        "privateKey": "**********-private.pem.key"
});

var clientTokenGet, clientTokenUpdate;
var shadowName = 'MyNewThing';

thingShadows.on('connect', function() {
    console.log('connect');
    thingShadows.register( shadowName );
    setTimeout( function() {
       clientTokenGet = thingShadows.get( shadowName );
    }, 2000 );
});

thingShadows.on('status', function(thingName, stat, clientToken, stateObject) {
    console.log('status');
    console.log('received '+stat+' on '+thingName+': '+
                 JSON.stringify(stateObject));
});

thingShadows.on('delta', function(thingName, stateObject) {
    console.log('delta');
    // LED ON/Off
    var state = stateObject.state.flash;
    ledVal = (state.led == null) ? ledVal: parseInt(state.led, 10);
    pwmVal = (state.pwm == null) ? pwmVal: parseInt(state.pwm, 10);
    console.log("delta stateObject.state", state);
    console.log("ledVal="+ledVal, "pwmVal="+pwmVal);
    if( ledVal  == 1 ){
        led.on();
    }else{
        led.off();
    }
    if(0 <= pwmVal && pwmVal <1024){
        console.log("pwm.write("+pwmVal+")");
        pwm.write(pwmVal);
    }
    console.log('received delta '+' on '+thingName+': '+
                JSON.stringify(stateObject));
    clientTokenUpdate = thingShadows.update( shadowName, {"state":{"reported": {"led": ledVal, "pwm": pwmVal}}});
});

thingShadows.on('timeout', function(thingName, clientToken) {
     console.log('received timeout '+' on '+operation+': '+
                 clientToken);
});
//終了時の処理
board.on("exit", function() {
    led.off();
    this.digitalWrite("P1-13", 0);
    this.digitalWrite("P1-15", 0);
    pwm.write(0);
});

サンプルソースshadowJ5LedMotor.jsを入力し終えたら、以下のコマンドで実行してみましょう。

$ sudo node shadowJ5LedMotor.js

1469651618861 Device(s) RaspberryPi-IO  
1469651618961 Connected RaspberryPi-IO  
1469651618972 Repl Initialized  

Shadow Statusで、キー"flash": { "led": 1, "pwm": 100 }内のledキーもしくはpwmキーを変化させてみてください。
デルタ分(:差分:変化したキー)のみ更新されるのがわかります!