趣味のログ

自分用の作業ログ。。

RaspberryPi3からGoogle Cloud IoTへGrovePi+のセンサデータを送信

RaspberryPi3につないだGrovePi+のセンサデータをGCP Cloud IoTへ送信した。
以下、Cloud IoTのQuickStart(https://cloud.google.com/iot/docs/quickstart?hl=ja)に従って進める。
※Rasberry Pi3はModel B(+ではない)。OSはraspbian。

cat /etc/debian_version
=>8.0

事前準備

プロジェクト作成(既存でもOK)、課金を有効、Cloud IoT Core and Cloud Pub/Sub API(複数)を有効にする。

バイス側の設定

Google Cloud SDKインストール

手順:https://cloud.google.com/sdk/docs/?hl=ja#deb
DebianUbuntu」タブの内容に従いインストールする。
※毎回exportするのは面倒なので、今回は、~/.profileに追記した。また、追加コンポーネントのインストールはなし。

echo "export CLOUD_SDK_REPO=\"cloud-sdk-$(lsb_release -c -s)\"" >> ~/.profile
exec bash -l
echo "deb http://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
sudo apt-get update && sudo apt-get install google-cloud-sdk
gcloud init

gcloud initを実行すると、ブラウザが開いてGoogleへのログインを求められる。
事前準備で作成したプロジェクトのアカウントでログインすると、Google Cloud SDKが許可を求める画面に遷移するので、「許可」ボタンを押すと認証完了。

コンソールに戻ると、対話形式で、使用するプロジェクト、リージョン/ゾーンを聞かれる。
今回は、事前準備で作成したプロジェクト、asia-northeast1-b(東京リージョンのbゾーン)、を選択した。

※リージョン/ゾーンによって、マシンのスペックも変わるので注意(選択可能なリージョン/ゾーン一覧:https://cloud.google.com/compute/docs/regions-zones/regions-zones#available)。

Node.jsインストール

インストールは下記記事を参照。2018/03/22現在、最新のLTSはv8.10.0 kmth23.hatenablog.com

node -v
=>v8.10.0
npm -v
=>5.6.0

バイスの登録

Cloud IoTのコンソール(https://console.cloud.google.com/iot?hl=ja)で作業。
まず「レジストリ」を作成し、「レジストリ」にデバイスを追加する。

「Create device registry.」ボタンを押して下記のように設定。

「作成」ボタンを押す。(これでレジストリの作成が完了)
遷移したページで「端末を追加」ボタンを押して下記のように設定。

  • 端末 ID: 任意のデバイスID
  • 端末の通信: 許可
  • 認証: 設定しない(デフォルトのままにする)
  • 端末メタデータ: 設定しない(デフォルトのままにする)

「追加」ボタンを押す。(これでデバイスの登録が完了) コンソールはこのまま開いておく。

バイスに公開鍵を追加

opensslがない場合は、事前にインストールする。 下記のコマンドで、rsa_cert.pem(公開鍵)、rsa_private.pem(秘密鍵)を作成する。

cd /tmp
openssl req -x509 -newkey rsa:2048 -keyout rsa_private.pem -nodes -out rsa_cert.pem -subj "/CN=unused"

開いたままにしておいたコンソールで「公開鍵を追加」ボタンを押し、rsa_cert.pemの内容をコピーして張り付ける。

  • 入力方法: 手動で入力
  • 公開鍵の形式: RS256_X509
  • 公開鍵の有効期限: 設定しない

「追加」ボタンを押す。

サンプルで動作確認

サンプルをgit cloneで取得する。gitがない場合は、事前にインストールする。
下記コマンドについては、これまでに作成した情報に従い、<<PROJECT_ID>>をプロジェクトID、<<TOPIC_NAME>>をトピック名、<<REGISTRY_ID>>をレジストリID、<<DEVICE_ID>>をデバイスIDに置き換えること。 <<任意のサブスクリプション名>>は、任意のサブスクリプション名に置き換えること。

また、jsファイルの実行時には、--cloudRegionの指定を忘れないこと(デフォルトはus-central1。今回はasia-east1にしたので指定が必要)。
--numMessagesで送信するデータ数を指定できる。今回はテストのため1メッセージだけ送信する。

cd 任意の作業dir
git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples
cd nodejs-docs-samples/iot/mqtt_example
cp /tmp/rsa_private.pem .
npm install
gcloud pubsub subscriptions create \
    projects/<<PROJECT_ID>>/subscriptions/<<任意のサブスクリプション名>> \
    --topic=projects/<<PROJECT_ID>>/topics/<<TOPIC_NAME>>
node cloudiot_mqtt_example_nodejs.js \
    --projectId=<<PROJECT_ID>> \
    --registryId=<<REGISTRY_ID>> \
    --deviceId=<<DEVICE_ID>> \
    --privateKeyFile=rsa_private.pem \
    --numMessages=1 \
    --algorithm=RS256 \
    --cloudRegion=asia-east1
gcloud pubsub subscriptions pull --auto-ack \
    projects/<<PROJECT_ID>>/subscriptions/<<任意のサブスクリプション名>>

jsファイルの実行で、1つのデータがpublishされる。gcloud pubsub subscriptions pullコマンドでsubscribeできれば成功。

センサー情報の取得

AWS IoTにデータ送信したときと同じ仕組みを使う。 kmth23.hatenablog.com

まず、pythonでGrovePi+につないだセンサー情報を取得する。
次に、動作確認で使ったJavaScriptコードを参考にして、mqttでCloud IoTへデータを送信するコードを書く。
pythonコードの実行は、AWS IoTの時と同様、child_processのexecファンクションを使用する。

まずは、package.jsonを作成し、必要なライブラリをインストール。

{
  "name": "grovepi-test",
  "version": "0.0.1",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "jsonwebtoken": "7.4.1",
    "mqtt": "2.15.0",
    "yargs": "8.0.2"
  },
  "devDependencies": {}
}
npm install

次に、実行ファイルを、index.jsとして作成。
5分間隔でセンサーデータを取得し、Cloud IoTへ送信する。
pythonコードは、/path/to/python/script.py。

// This software includes the work that is distributed in the Apache License 2.0

'use strict';

const fs = require('fs');
const jwt = require('jsonwebtoken');
const mqtt = require('mqtt');
const exec = require('child_process').exec;

var argv = require(`yargs`)
  .options({
    projectId: {
      default: process.env.GCLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT,
      description: 'The Project ID to use. Defaults to the value of the GCLOUD_PROJECT or GOOGLE_CLOUD_PROJECT environment variables.',
      requiresArg: true,
      type: 'string'
    },
    cloudRegion: {
      default: 'us-central1',
      description: 'GCP cloud region.',
      requiresArg: true,
      type: 'string'
    },
    registryId: {
      description: 'Cloud IoT registry ID.',
      requiresArg: true,
      demandOption: true,
      type: 'string'
    },
    deviceId: {
      description: 'Cloud IoT device ID.',
      requiresArg: true,
      demandOption: true,
      type: 'string'
    },
    privateKeyFile: {
      description: 'Path to private key file.',
      requiresArg: true,
      demandOption: true,
      type: 'string'
    },
    algorithm: {
      description: 'Encryption algorithm to generate the JWT.',
      requiresArg: true,
      demandOption: true,
      choices: ['RS256', 'ES256'],
      type: 'string'
    },
    mqttBridgeHostname: {
      default: 'mqtt.googleapis.com',
      description: 'MQTT bridge hostname.',
      requiresArg: true,
      type: 'string'
    },
    mqttBridgePort: {
      default: 8883,
      description: 'MQTT bridge port.',
      requiresArg: true,
      type: 'number'
    },
    messageType: {
      default: 'events',
      description: 'Message type to publish.',
      requiresArg: true,
      choices: ['events', 'state'],
      type: 'string'
    }
  })
  .example(`node $0 cloudiot_mqtt_example_nodejs.js --projectId=blue-jet-123 \\\n\t--registryId=my-registry --deviceId=my-node-device \\\n\t--privateKeyFile=../rsa_private.pem --algorithm=RS256 \\\n\t --cloudRegion=us-central1`)
  .wrap(120)
  .recommendCommands()
  .epilogue(`For more information, see https://cloud.google.com/iot-core/docs`)
  .help()
  .strict()
  .argv;

function createJwt (projectId, privateKeyFile, algorithm) {
  const token = {
    'iat': parseInt(Date.now() / 1000),
    'exp': parseInt(Date.now() / 1000) + 20 * 60, // 20 minutes
    'aud': projectId
  };
  const privateKey = fs.readFileSync(privateKeyFile);
  return jwt.sign(token, privateKey, { algorithm: algorithm });
}

const mqttClientId = `projects/${argv.projectId}/locations/${argv.cloudRegion}/registries/${argv.registryId}/devices/${argv.deviceId}`;
const mqttTopic = `/devices/${argv.deviceId}/${argv.messageType}`;

let connectionArgs = {
  host: argv.mqttBridgeHostname,
  port: argv.mqttBridgePort,
  clientId: mqttClientId,
  username: 'unused',
  password: createJwt(argv.projectId, argv.privateKeyFile, argv.algorithm),
  protocol: 'mqtts',
  secureProtocol: 'TLSv1_2_method'
};

let client = mqtt.connect(connectionArgs);
client.subscribe(`/devices/${argv.deviceId}/config`);

client.on('connect', (success) => {
  console.log('connect');
  if (!success) {
    console.log('Client not connected...');
  } else {
    setInterval(() => {
      exec('python /path/to/python/script.py', (error, stdout, stderr) => {
        if (error !== null) {
          console.log('exec error: ' + error);
          return
        }
        var data = stdout.replace(/\r?\n/g,"");
        var datas = data.split(",")
        var record = {
          registryid: argv.registryId,
          deviceid: argv.deviceId,
          timestamp: datas[0],
          temperature: Number(datas[1]),
          humidity: Number(datas[2]),
          moisture: Number(datas[3]),
          light: Number(datas[4]),
          location: datas[5] + "," + datas[6]
        };
        const payload = JSON.stringify(record);
        console.log("Publish: " + payload);
        client.publish(mqttTopic, payload, { qos: 1 });
      });
      return;
    }, 300000);
  }
});

client.on('close', () => {
  console.log('close');
});

client.on('error', (err) => {
  console.log('error', err);
});

client.on('message', (topic, message, packet) => {
  console.log('message received: ', Buffer.from(message, 'base64').toString('ascii'));
});

client.on('packetsend', () => {
  // Note: logging packet send is very verbose
});

実行コマンドは下記。rsa_private.pemはあらかじめコピーしておくこと。

cp /tmp/rsa_private.pem .
npm start \
    --projectId=<<PROJECT_ID>> \
    --registryId=<<REGISTRY_ID>> \
    --deviceId=<<DEVICE_ID>> \
    --privateKeyFile=rsa_private.pem \
    --algorithm=RS256 \
    --cloudRegion=asia-east1

subscribeすると、下記のようなデータが取得できるはず。

gcloud pubsub subscriptions pull --auto-ack \
    projects/<<PROJECT_ID>>/subscriptions/<<任意のサブスクリプション名>>
=>{
    "registryid":"<<REGISTRY_ID>>",
    "deviceid":"<<DEVICE_ID>>",
    "timestamp":"2018-03-23T16:19:48+09:00",
    "temperature":25.2,
    "humidity":30.4,
    "moisture":0,
    "light":206
    }