AWS IoT Greengrassを使ってGoogleHomeにしゃべらせる

今回は、AWS IoT Greengrassの勉強を兼ねて、自宅にあるGoogleHomeにしゃべってもらいましょう。

これまでいろんな記事でGoogleHomeをしゃべらせてきましたが、GoogleHomeはローカルネットワーク上にある必要がありました。
したがって、いつも使っているAWS Lambdaに配置したNode.jsから、GoogleHomeにアクセスしようにも、ローカルネットワークにないのでダメでした。

そこで、GreengrassによってNode.jsのロジックをサーバ側のAWS Lambdaで管理しつつ、ローカルネットワークに配置するGreengrassコアにロジックをデプロイしてGoogleHomeを操ります。
また、IoTの特徴であるMQTTで起動させたりできますし、PublishするクライアントをX.509公開鍵ペアできちんと認証することができます。

全体像です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/968182a0-bfb3-d6a9-d382-780f660ccfc5.png)

いったん、LambdaをGreengrassコアにデプロイすれば、インターネット経由でのMQTT Publishにも反応し、ローカルネットワーク内からも直接MQTT Publishに反応してGoogleHomeがしゃべります。

# Greengrassコアのインストール

以下に記載されている通りにすれば大丈夫です。
私はUbuntuにインストールしてみました。

クイックスタート：Greengrassデバイスのセットアップ
　https://docs.aws.amazon.com/ja_jp/greengrass/latest/developerguide/quick-start.html

その前に、AWS認証情報を設定する必要があります。GreengrassのインストーラがGreengrassコアをインストールするのと同時に、AWS側に、インストールしたGreengrassコアを設定するためです。

```
export AWS_ACCESS_KEY_ID=【AWSアクセスキー】
export AWS_SECRET_ACCESS_KEY=【AWSアクセスキーシークレット】
export AWS_SESSION_TOKEN=【AWSセッショントークン】
```

一時的なセキュリティ情報であるセッショントークンを指定するようにとあります。IAMで発行したAWSアクセスキー＋AWSアクセスキーシークレット(長期認証情報)だけでも大丈夫ですが、前者が推奨だそうです。

以下で、インストールを実行します。

```
$ wget -q -O ./gg-device-setup-latest.sh https://d1onfpft10uf5o.cloudfront.net/greengrass-device-setup/downloads/gg-device-setup-latest.sh && chmod +x ./gg-device-setup-latest.sh && sudo -E ./gg-device-setup-latest.sh bootstrap-greengrass-interactive
```

インストール場所をどこにするか聞かれますが、とりあえずデフォルトの / にしてみました。そうすると、/greengrassというフォルダが作られてインストールされます。

greengrassdの起動方法は以下の通りです。

```
cd /greengrass/ggc/core/
sudo ./greengrassd start
```

Greengrass successfully started と表示されれば成功です。

自動起動したい場合は、systemdに登録しましょう。

```vi /etc/systemd/system/greengrassd.service```

```greengrassd.service
[Unit]
Description=Greengrass Daemon

[Service]
Type=forking
PIDFile=/var/run/greengrassd.pid
Restart=on-failure
ExecStart=/greengrass/ggc/core/greengrassd start
ExecReload=/greengrass/ggc/core/greengrassd restart
ExecStop=/greengrass/ggc/core/greengrassd stop

[Install]
WantedBy=multi-user.target
```

```
> systemctl enable greengrassd
> systemctl start greengrassd
```

インストール完了と同時に、AWSのWeb管理コンソールのAWS IoTにも登録が完了しています。

AWS IoT Web管理コンソール
　https://ap-northeast-1.console.aws.amazon.com/iot/home?region=ap-northeast-1

Greengrassのグループを選択すると、「GreengrassDeviceSetup_Group_XXXXXXXXX-XXXX-XXXX-XXXXXXXXXXXX」という名前のGreengrassグループが出来上がっているかと思います。

# node.js実行環境の準備とLambdaの登録

Greengrassコア上で動作する言語はNode.jsにしようと思います。そのために、Greengrassコア上でNode.jsが動作する環境にしておく必要があります。

https://github.com/aws/aws-greengrass-core-sdk-js
に設定方法が書いてあります。

Preparing your Greengrass to run NodeJS Lambda functions
に示してある通りに進めます。

まずnode.js v12をインストールします。
nvmを使っている場合は以下の通りです。
　```nvm install v12.18.0```

そうすると、以下にnodeがインストールされます。

> /home/XXXX/.nvm/versions/node/v12.18.0/bin/node


これを以下の場所に名前を変えてシンボリックリンクを作っておきます。

```
cd /usr/local/bin
ln -s /home/XXXX/.nvm/versions/node/v12.18.0/bin/node nodejs12.x
```

下記のようにnode.jsのバージョンが表示されればOKです。

```
$ nodejs12.x -v
v12.18.0
```

それでは、登録するNode.jsのソースコードを作成していきます。
以下、ZIPダウンロードしておきます。

aws/aws-greengrass-core-sdk-js
　https://github.com/aws/aws-greengrass-core-sdk-js

適当なフォルダで、

```
npm init -y
npm install cbor@5.0.1
vi index.js
```

先ほどダウンロードしたaws-greengrass-core-sdk-js-master.zip に含まれるフォルダaws-greengrass-core-sdk をnode_modules フォルダにコピーします。

index.js の内容です。

```js:index.js
'use strict';

const ggSdk = require('aws-greengrass-core-sdk');

const Client = require('castv2-client').Client;
const MediaReceiver = require('castv2-client').DefaultMediaReceiver;
const googletts = require('google-tts-api');

const GOOGLE_DEVICE_ADDRESS = process.env.GOOGLE_DEVICE_ADDRESS || 【GoogleHomeのIPアドレス】;

exports.handler = async (event, context) =>{
  console.log(JSON.stringify(event));
  console.log(JSON.stringify(context));
  
  console.log("topic=" + context.clientContext.Custom.subject);

  return homeSpeech(event.message, event.address || GOOGLE_DEVICE_ADDRESS);
};

function homeSpeech(text, host) {
  return googletts(text, 'ja-JP', 1)
    .then(function(url) {
      return playUrl(url, host);
    });
}

function playUrl(url, host) {
  return new Promise((resolve, reject) => {
    var client = new Client();
    client.connect(host, () => {
      client.launch(MediaReceiver, (err, player) => {
        if( err ){
          console.log('Error: %s', err.message);
          client.close();
          return reject(err);
        }

        var media = {
          contentId: url,
          contentType: 'audio/mp3',
          streamType: 'BUFFERED'
        };
        player.load(media, { autoplay: true }, (err, status) =>{
          client.close();
          resolve('Device notified');
        });
      });
    });

    client.on('error', (err) =>{
      console.log('Error: %s', err.message);
      client.close();
      reject(err);
    });
  })
}
```

見ての通り、以下の2つのnpmモジュールを使っています。

> npm install castv2-client
> npm install google-tts-api

【GoogleHomeのIPアドレス】のところには、しゃべらせたいGoogleHomeのIPアドレスを指定してください。AndroidのGoogleHomeアプリから、対象デバイスの設定画面からわかります。

MQTT Publishなど外部から起動のトリガを受け付けると、``` exports.handler```が呼び出されます。
受信したメッセージのトピック名は、``` context.clientContext.Custom.subject```にあります。メッセージの内容は、eventにあります。MQTT Publish側がJSON文字列で送ると、オブジェクトになってeventに設定されます。

(参考) 今回は使っていませんが。。。
　https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Greengrass.html

後は、いつもの通り、ZIPに固めてAWS Lambdaに登録します。
その時、Node.jsのバージョンはv12を選択します。

Lambda関数名は、例えば、「Greengrass_GoogleHome」とでもしておきます。
そして、「新しいバージョンを発行」を選択して発行しておきます。コードを更新するたびに1から順に振られます。

#AWS IoT GreengrassにLambdaを登録

AWS IoT Greengrassの管理ページから、Greengrass - グループ - Lambdaを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/35f06df9-93fe-87ca-ba38-ee8911a366d1.png)

Lambdaの追加ボタンを押下して、既存のLambda関数の使用 から、先ほど登録したLambda関数 Greengrass_GoogleHomeを選択します。上記の絵は、選択後の画面です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/cadea881-de73-49a6-89a8-c13f5f26b0ce.png)

次に、Lambdaを起動するトリガを作成します。
サブスクリプションから行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/3b24bd96-189b-0022-93a4-8222ff0f7541.png)

「サブスクリプションの追加」ボタンを押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/90377776-aa2a-281f-d0a7-25edecd1b2ee.png)

ソースとして、サービスのIoT Cloud、ターゲットとしてLambdaのGreengrass_GoogleHomeを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/8bd4e96c-c7d1-4fba-f54a-fafd8b7f5ebd.png)

そして、トピック名に適当に「hello/home」と入力します。

これで準備ができたので、右上のアクションからデプロイを選択します。
ちょっと時間がたつと、左上に「〇正常に完了しました」、と出たらOKです。ローカルのUbuntuにLambdaがデプロイされたはずです。

# Lambdaの実行

IoT Cloudから起動できるようにしました。
AWS IoTのWeb管理コンソールのテストから実行ができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/26670c7e-bba4-4344-56e1-d11028d191e3.png)

トピックへ発行 を選択し、
　トピック名：hello/home
　メッセージ：{ “message”: “こんにちは” }
と入力し、「トピックに発行」ボタンを押下してみましょう。GoogleHomeがしゃべってくれましたでしょうか？

だめだったら、Ubuntuの以下のログファイルを確認してみましょう。

```
/greengrass/ggc/var/log/system/runtime.log
/greengrass/ggc/var/log/user/ap-northeast-1/XXXXXXXXXXXX/Greengrass_GoogleHome.log
```

XXXXXXXXXXXXはAWSアカウント番号です。
ちなみに、LambdaのNode.jsでconsole.logで出力した文字列は、上記の後者のログファイルに出力されます。

#インターネット上からGoogleHomeにしゃべらせる

次に、X.509証明書で認証したデバイスからMQTT PublishしてGoogleHomeからしゃべらせます。

まず、Greengrassグループにデバイスを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/25f4a8cd-44a4-0e48-dcef-ee32cc02d1c4.png)

新しいデバイスの作成 ボタンを押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/7ca0e595-992b-2a2e-e0d0-639662c34e9f.png)

デバイス名は適当に「HelloWorld_Publisher」としました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/8fc94400-f77f-881f-a3b0-2dca8cc73f31.png)

1-Clickデプロイで行きましょう。
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/2b0a7a41-6068-34ed-7127-90dda35e23c2.png)


これらリソースはtar.gzとしてダウンロードしてください でダウロードしておきましょう(X.509証明書類)。
ルートCAからも証明書をダウンロードしておきます。「Amazon ルート CA 1」でよいです。

次に、このデバイス「HelloWorld_Publisher」からIoT CloudにPublishできるようにしましょう。
Greengrassグループのサブスクリプションに設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/cf9cee61-1ae0-51b6-e8fa-17fdae0cd017.png)

ソースとして、GreengrassデバイスのHelloWorld_Publisher、ターゲットとして、サービスのIoT Cloudを選択します。トピックには先ほどと同じ「hello/home」を指定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/43caab79-5af1-0331-2299-bde79e51f138.png)

また、アクションからデプロイをしましょう。

そうそう、IoT CloudのURLをメモっておきましょう。
AWS IoTのWeb管理コンソールの設定を選ぶと表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/ca1ef323-5643-71b3-d420-d41f857e235c.png)

こんな感じのURLです。
　```XXXXXXXXXXXXX-ats.iot.ap-northeast-1.amazonaws.com```

MQTT PublishするNode.jsのソースを示します。
npmモジュールのmqttを使っています。

> npm install mqtt

```js:index.js
'use strict';

const mqtt = require('mqtt');
const fs = require('fs');
var ROOT_CA = fs.readFileSync("【ルートCAの証明書ファイル】");
var PRIVATE_KEY = fs.readFileSync(【X.509証明書の秘密鍵ファイル】);
var CERT = fs.readFileSync(【X.509証明書ファイル】);

const AWSIOT_ENDPOINT = 【AWS IoTのエンドポイント】;
const DEVICE_NAME = 'HelloWorld_Publisher';

var options={
  clientId: DEVICE_NAME,
  rejectUnauthorized : false,
  key: PRIVATE_KEY,
  cert: CERT,
  ca: ROOT_CA,
};

var client  = mqtt.connect("mqtts://" + AWSIOT_ENDPOINT + ":8883",options);
client.on("connect",function(){	
  console.log("connected  "+ client.connected);
  
  var topic = "hello/home";
  var message = { message : 'こんばんは' };
  client.publish(topic, JSON.stringify(message));
  client.end();
  console.log('OK');
});
```

・X.509証明書の秘密鍵ファイル
・X.509証明書ファイル
→ 先ほどダウンロードした、X.509証明書のtar.gzに入っているファイルです。

・ルートCAの証明書ファイル
→ 上記と一緒にダウンロードしたやつです。

・AWS IoTのエンドポイント
→ 先ほどメモったやつです。

さあ、実行してみましょう。X.509証明書類は、Node.jsと同じフォルダに置いておきましょう。

> node index.js

しゃべってくれましたか？

#ローカルネットワークからGoogleHomeにしゃべらせる

今度は、IoT Cloudを介さずに、ローカルネットワーク上のデバイスからGreengrassコアに直接MQTT PublishしてGoogleHomeにしゃべらせましょう。

そのためには、まず、Greengrassコア自体に、MQTT Publishを受け付けるエンドポイントを設定する必要があります。
Greengrassグループを選択し、設定を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/7cfef1d7-2a67-ad22-1569-9487ff8d85ae.png)
 

ローカル接続の検出のところで、接続情報の手動設定を選択し、特定のエンドポイント情報のCoreの表示 を選択します。
Coreは1つのみあると思いますので、それを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/fd9abcc8-a53b-d4ab-f91a-da902ea280e8.png)

設定を選択します。
ここに、GrenngrassコアをインストールしたUbuntuのIPアドレスと、ポート番号として8883を指定し追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/3b5c40e5-a880-908a-21c8-f093256427ea.png)

次に、デバイスから直接Lambdaに届くサブスクリプションを作成しましょう。
サブスクリプションを選択し、サブスクリプションを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/d1bc0c4c-5ede-fff1-b7a4-51df6853637a.png)

ソースとして、GreengrassデバイスのHelloWorld_Publisherを選択します。
ターゲットとして、LambdaのGreengrass_GoogleHomeを選択します。
トピック名には、同じように「hello/home」を指定しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/e995e02e-67f6-13a4-c367-30525dec042b.png)

準備ができました。アクションからデプロイしましょう。

以下を実行しましょう。

```js:index.js
'use strict';

const mqtt = require('mqtt');
const fs = require('fs');
var ROOT_CA = fs.readFileSync("【ルートCAの証明書ファイル】");
var PRIVATE_KEY = fs.readFileSync(【X.509証明書の秘密鍵ファイル】);
var CERT = fs.readFileSync(【X.509証明書ファイル】);


const GREENGRASS_ENDPOINT = 【GreengrassコアのIPアドレス】;
const DEVICE_NAME = 'HelloWorld_Publisher';

var options={
  clientId: DEVICE_NAME,
  rejectUnauthorized : false,
  key: PRIVATE_KEY,
  cert: CERT,
  ca: ROOT_CA,
};

var client  = mqtt.connect("mqtts://" + GREENGRASS_ENDPOINT + ":8883",options);
client.on("connect",function(){	
  console.log("connected  "+ client.connected);
  
  var topic = "hello/home";
  var message = { message : 'こんばんは' };
  client.publish(topic, JSON.stringify(message));
  client.end();
  console.log('OK');
});
```

ご覧の通り、AWSIOT_ENDPOINTの代わりにGREENGRASS_ENDPOINTを指定しています。GREENGRASS_ENDPOINTに、GreengrassコアをインストールしたUbuntuのIPアドレスを指定します。

> node index.js

どうでしょう。GoogleHomeはしゃべってくれましたでしょうか？

ちなみに、GreengrassコアをインストールしたUbuntuのIPアドレスは以下でも取得できます。

```js:index.js
'use strict';

const https = require('https');
const fs = require('fs');
var ROOT_CA = fs.readFileSync("【ルートCAの証明書ファイル】");
var PRIVATE_KEY = fs.readFileSync(【X.509証明書の秘密鍵ファイル】);
var CERT = fs.readFileSync(【X.509証明書ファイル】);

var opt = {
  hostname: 'greengrass-ats.iot.ap-northeast-1.amazonaws.com',
  port : 8443,
  path: '/greengrass/discover/thing/HelloWorld_Publisher',
  method: 'GET',
  cert: CERT,
  key: PRIVATE_KEY,
  ca: ROOT_CA,
};

do_get( opt )
.then(data =>{
  console.log(data.toString());
});

function do_get(opt){
  return new Promise((resolve, reject) =>{
    const req = https.request(opt, (res) =>{
      if( res.statusCode != 200 )
        return reject('status is not 200.');

      var body = Buffer.from([]);
    
      res.on('data', (chunk) =>{
        body = Buffer.concat([body, chunk]);;
      });

      res.on('end', () =>{
        return resolve(body);
      });
    });
    
    req.on('error', (error) =>{
      console.log(error);
      return reject(error);
    });
    
    req.end();
  });
}
```

optのpathにデバイス名を指定しています。

(参考) Greengrass Discovery RESTful API
https://docs.aws.amazon.com/ja_jp/greengrass/latest/developerguide/gg-discover-api.html

以上