Raspberry Pi のCPU温度をfirebaseの Cloud Firestore に記録してVue.jsでグラフ表示する

自宅に設置したRaspberry PiからCPU温度を定期発信し、firebaseの Cloud Firestore に記録します。記録した温度の情報を、Vue.jsコンポーネントにグラフで表示します。

完成図

f:id:kojimainjp:20181226192934p:plain




注意

Firestoreがアーキテクチャ選択的に微妙だったので、こちらでやり直しています。
kojimainjp.hatenablog.com



(それでも読む場合↓↓↓)



本記事ではFirebaseの無料プランを利用しています。現時点ではクレジットカード登録してアップグレードしない限りにおいて料金が発生することは無いはずですが、実践する場合は自己責任でお願いします。

動機

自身のポートフォリオサイトに載せるネタの一つとして始めました。(※)
そのポートフォリオがこちらです。

f:id:kojimainjp:20181221092948p:plain
https://github.com/kojimain/kojimain-portfolio

技術を追っかけるために一人遊んでいる分には楽しいのですが、中身の薄ーい、希釈しすぎた粉ジュースのようなサイトになっています。
何か見栄えのする題材が無いかなと思い、部屋に転がっていた Raspberry Pi を利用することにし、タイトルの題材に決めました。

目次

注意

本記事は個人の趣味の範囲で書かれています。誰かの参考になれば幸いではありますが、実践は自己責任にてお願いします。
また、本記事ではFirebaseの無料プランを利用しています。現時点ではクレジットカード登録してアップグレードしない限りにおいて料金が発生することは無いはずですが、こちらも自己責任でお願いします。




firebaseプロジェクト立ち上げ、DB作成


まずはfirebaseプロジェクトを作成します。

firebaseコンソールにアクセスします。
https://console.firebase.google.com/?hl=ja

こんな感じでプロジェクト追加します。
(黒塗りしていますが、最終的にフロントのソースに出る値なので秘匿性は無いです。)
f:id:kojimainjp:20181221110625p:plain

続いて、Raspberry Pi から送信する温度データを格納するためのDBを、Cloud Firestore で作成します。

左のメニュー>開発>databaseへ
Firestoreの作成ボタンがあるのでクリックし、「ロックモードで開始」のまま作成します。
f:id:kojimainjp:20181221131110p:plain

このような画面になるので、「コレクションを追加」します。
f:id:kojimainjp:20181221132318p:plain

"rasp_temperatures"コレクションを追加します。
f:id:kojimainjp:20181221133633p:plain

すると、ドキュメント第一号を登録するよう促されます。
以下のように登録します。

  • ドキュメントID: ユニークなID(自動生成を押す)
  • value: 温度値(number型 単位は℃)
  • sentAt: 送信日時

f:id:kojimainjp:20181223184113p:plain

更にもう1ドキュメントしておきます。
「ドキュメントを追加」から、以下の内容で追加します。
f:id:kojimainjp:20181223184220p:plain

これで、温度のプロット値を格納する入れ物となるDBが完成しました。
f:id:kojimainjp:20181223184258p:plain



Firebase Admin SDK で温度送信用スクリプトを書く


Raspberry Pi から Cloud Firestore に温度データを書き込むためのスクリプトを、ローカルで作成します。

初めに、Firebase Admin SDK を操作するためのサービスアカウントを作成します。
左のメニュー>設定アイコン>プロジェクトの設定>サービスアカウントタブへ
「新しい秘密鍵の生成」からキーを生成すると、credential情報のjsonファイルがDLされます。
この後の手順で使用するので保管しておきます。
f:id:kojimainjp:20181223150937p:plain

続いて、温度送信用スクリプトをNode.jsで開発していきます。
まずローカルの任意の場所にプロジェクトフォルダを作成し、yarn init します。

$ cd path/to/workdir
$ mkdir send_temperature
$ cd send_temperature
$ yarn init
(全部エンター)

今回のスクリプトに必要なパッケージを追加します。

$ yarn add firebase-admin

スクリプトを書いていきます。
index.jsを以下の内容で作成します。
60秒おきにCPU温度をfirestoreに送信する、という内容になっています。
CPU温度取得の処理はダミーです。後でラズパイに配置するときに実際の処理に書き換えます。

// setup
const admin = require("firebase-admin")
const serviceAccount = require("./credentials/serviceAccount.json")
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: `https://${serviceAccount.project_id}.firebaseio.com`
})
const firestore = admin.firestore()
firestore.settings({timestampsInSnapshots: true})

// functions
// ダミーのCPU温度取得処理
const fetchTemperature = function() {
  const array = [41.0, 41.5, 42.0]
  return array[Math.floor(Math.random() * array.length)]
}
// CPU温度送信処理
const sendTemperature = function() {
  const value = fetchTemperature()
  const sentAt = new Date()
  console.log(`${sentAt}: ${value}`)
  firestore.collection('rasp_temperatures')
  .add({
    value: value,
    sentAt: sentAt
  })
}

// main
sendTemperature()
setInterval(function() {
  sendTemperature()
}, 60000)

加えて、credentials/serviceAccount.jsonに、先ほどDLしたcredential情報のjsonファイルを保存します。

以上が整ったら、ローカルで実行します。
コマンド直後に一度温度が送信され、以後1分間隔で送信を続けます。

$ node index.js
Tue Dec 25 2018 09:59:57 GMT+0900 (GMT+09:00): 41
Tue Dec 25 2018 10:00:57 GMT+0900 (GMT+09:00): 41.5
...
(終了はctrl-c)  

Cloud Firestore のデータを確認すると、データが追加されています。
f:id:kojimainjp:20181225100713p:plain

以上で温度送信の方法が確立できました。
今回作成したスクリプトは、Raspberry Pi でcronの定期実行を設定するときに再登場します。



Vue.js でグラフ表示する


Cloud Firestore に登録された温度データを表示するグラフのVue.jsコンポーネントを作成していきます。

まずローカルの任意の場所にVue.jsコンポーネント開発環境を整えます。
vue-cli v3が必要になりますので、グローバルにインストールします。

$ yarn global add @vue/cli @vue/cli-service-global
$ cd path/to/workdir
$ vue create temperature_graph

? Please pick a preset: (Use arrow keys)
❯ default (babel, eslint)
  Manually select features

...(完了)

$ cd temperature_graph
$ tree -L 1
.
├── README.md
├── babel.config.js
├── node_modules
├── package.json
├── public
├── src
└── yarn.lock

必要なパッケージを追加します。

yarn add firebase c3 vue-c3

グラフ描画に「c3」というライブラリを使用します。
(こちらの記事を参考にさせて頂きました。)

開発環境が整ったので、グラフ描画用コンポーネントを作成していきます。
src/components/Graph.vueを以下の内容で作成します。

(追記:2018/12/29)
以下コードの.limit(12)の箇所を、当初.limit(288)で掲載していましたが、「読み取りオペレーション数」の指標が無料枠に収まらない速度で上昇したため、一度に読み取る数を減らしました。
(読み取り回数はcollection単位でカウントされると思い込んでいましたが、doc.data()でdocumentを取得する度にカウントされるようです。)

<template>
  <div>
    <vue-c3 :handler="handler"/>
  </div>
</template>

<script>
  import Vue from 'vue'
  import firebase from 'firebase/app'
  import 'firebase/firestore'
  import VueC3 from 'vue-c3'
  import 'c3/c3.min.css'

  // firebase
  firebase.initializeApp({
    projectId: 'portfolio-xxx'
  })
  const firestore = firebase.firestore()
  firestore.settings({timestampsInSnapshots: true})

  export default {
    components: {
      VueC3
    },
    data() {
      return {
        temperatures: [],
        handler: new Vue()
      }
    },
    methods: {
      async getTemperatures() {
        await firestore.collection('rasp_temperatures')
        .orderBy('sentAt', 'asc')
        .limit(12)
        .get()
        .then(querySnapshot => {
          this.temperatures = querySnapshot.docs.map(doc => {
            const data = doc.data()
            return {
              value: data.value,
              sentAt: data.sentAt.toDate()
            }
          })
        })
      },
      async initGraph() {
        await this.getTemperatures()
        const options = {
          data: {
            x: 'x',
            columns: [
              ['x'].concat(this.temperatures.map(t => { return t.sentAt })),
              ['温度(℃)'].concat(this.temperatures.map(t => { return t.value }))
            ]
          },
          axis: {
            x: {
              type: 'timeseries',
              tick: {
                format: '%Y-%m-%d %H:%M'
              }
            }
          }
        }
        this.handler.$emit('init', options)
      }
    },
    mounted() {
      this.initGraph()
    }
  }
</script>

作成後、以下で起動します。

$ vue serve src/components/Graph.vue

http://localhost:8080/で確認すると、エラーが出てグラフが描画されません。
Firestoreを「ロックモードで開始」で作成しているので、フロントエンドから参照できない状態になっています。

フロントエンドから参照できるよう設定していきます。
左のメニュー>開発>database>ルールタブを選択します。
f:id:kojimainjp:20181222213015p:plain

このようになっているので、以下に書き換えて登録します。
rasp_temperaturesのlist表示のみを許可するよう設定しました。

service cloud.firestore {
  match /databases/{database}/documents {
    match /rasp_temperatures/{rasp_temperature} {
      allow list;
    }
  }
}

改めて、http://localhost:8080/で確認します。
まだプロットが少ないので少々いびつですが、欲しいグラフが得られました。
f:id:kojimainjp:20181225100957p:plain



Raspberry Pi にデータ送信スクリプトを配置する


最後に、自宅に設置したRaspberry Pi にCPU温度送信スクリプトを配置し常駐稼働させます。

まず、先に作成した温度送信スクリプトの温度取得がダミー処理なので、これを実処理にしていきます。

CPU温度は以下の方法で取得できます。
単位は摂氏の1000倍なので、以下の例では「46.1℃」となります。

(ラズパイで実行)
$ cat /sys/class/thermal/thermal_zone0/temp
46160

これを利用し、index.jsを以下のように編集します。
CPU温度を/sys/class/thermal/thermal_zone0/tempから取得するようにしました。
併せて、送信間隔を300000ミリ秒(5分)と伸ばしました。

(追記:2018/12/29)
上記変更に加え、main処理でsetInterval前に初回のsendTemperature()を実行していたのを削除しました。

// setup
const admin = require("firebase-admin")
const serviceAccount = require("./credentials/serviceAccount.json")
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: `https://${serviceAccount.project_id}.firebaseio.com`
})
const firestore = admin.firestore()
firestore.settings({timestampsInSnapshots: true})
const execSync = require('child_process').execSync

// functions
// CPU温度取得処理
const fetchTemperature = function() {
  const tempOrg = execSync('cat /sys/class/thermal/thermal_zone0/temp').toString()
  return parseFloat(`${tempOrg.slice(0,2)}.${tempOrg.slice(3,4)}`)
}
// CPU温度送信処理
const sendTemperature = function() {
  const value = fetchTemperature()
  const sentAt = new Date()
  console.log(`${sentAt}: ${value}`)
  firestore.collection('rasp_temperatures')
  .add({
    value: value,
    sentAt: sentAt
  })
}

// main
// sendTemperature() ←削除
setInterval(function() {
  sendTemperature()
}, 300000)

以上で温度送信スクリプトの実処理が完成しました。
ここから、実際にRaspberry Piスクリプトを配置して稼働させます。

3記事目にしてようやく主役のRaspberry Pi が登場します。
OSはRaspbianを入れてあります。
宅内LANに接続してあり、同じく宅内LANに接続した別PC端末からssh接続できるようになっています。
(Raspberry Pi セットアップ方法はメインでないため割愛します。)
f:id:kojimainjp:20181226093612j:plain

Raspberry Pi にyarnを入れます。
公式のインストール手順に従います。

(ラズパイにssh接続)
$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
$ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
$ sudo apt-get update && sudo apt-get install yarn

続いて、先の記事で作成したデータ送信スクリプトを配置します。
ラズパイ上に持ってくる手段は何でも良いんですが、筆者はgithub公開リポジトリを建てたのでそこから持ってきます。
追記:2019/1/6
削除しました。
rootユーザで作業します。

$ sudo su -
# cd /usr/local/src
# git clone https://github.com/kojimain/rasp_send_temperature.git
# cd rasp_send_temperature
# vi credentials/serviceAccount.json
(ローカルからjsonの中身をコピペして:wq)

npmパッケージをインストールします。

# yarn install

ここで一度、送信してみます。

# node index.js
Wed Dec 26 2018 11:09:02 GMT+0900 (JST): 41.5
(終了はctrl-c)  

Raspberry Pi からCPU温度送信が行えました。
グラフにデータが追加され、無事にクリスマスを横断していきました。
f:id:kojimainjp:20181226112034p:plain

最後に、起動サービスに登録して常駐化させます。

# vi /etc/systemd/system/rasp_send_temperature.service

以下の内容で保存します。 (「/usr/bin/node」は# which nodeの値)

[Unit]
Description = rasp_send_temperature

[Service]
ExecStart = /usr/bin/node /usr/local/src/rasp_send_temperature/index.js
Restart = always
Type = simple

[Install]
WantedBy = multi-user.target

自動起動ON、起動、確認します。
これで、OS起動時に自動で立ち上がり、5分おきに温度が送信されるようになります。

# systemctl enable rasp_send_temperature
# systemctl start rasp_send_temperature
# systemctl status rasp_send_temperature
● rasp_send_temperature.service - rasp_send_temperature
   Loaded: loaded (/etc/systemd/system/rasp_send_temperature.service; enabled; vendor preset: enabled)
   Active: active (running) since Wed 2018-12-26 14:39:52 JST; 5s ago
 Main PID: 13385 (node)
   CGroup: /system.slice/rasp_send_temperature.service
           └─13385 /usr/bin/node /usr/local/src/rasp_send_temperature/index.js

12月 26 14:39:52 raspberrypi systemd[1]: Started rasp_send_temperature.
12月 26 14:39:55 raspberrypi node[13385]: Wed Dec 26 2018 14:39:55 GMT+0900 (JST): 49.8

数分経過した図です。
f:id:kojimainjp:20181226151230p:plain

(注) データ自動削除の仕組みがまだないので、確認が終わったら停止して無効化します。

# systemctl stop rasp_send_temperature
# systemctl disable rasp_send_temperature




まとめ

以上で、表題の内容としては全て達成しました。
まだ詰まり切っていない部分として、以下のような課題があります。
これらはいずれ着手しようと思います。

  • グラフのx軸の文字が重なっている
    読めないので、見た目の修正が必要です。
  • Firestoreの各制限
    read(50,000/日)やwrite(20,000/日)のAPI制限は十分ですが、保存容量(1 GiB)はこのままだといずれ超えてしまいます。Cloud Functions で Firestore 書き込みのイベントを拾って、古い日付から自動消去するような仕組みを考えています。

冒頭の繰り返しになりますが、本記事は個人の趣味の範囲で書かれています。
誰かの参考になれば幸いですが、間違いがありましたらご指摘頂けますと大変助かります!

追記:2018/12/27

続編を書きました。
古い日付から自動消去する仕組みを構築しました。
kojimainjp.hatenablog.com

また、Vueコンポーネントを少々加工して、ポートフォリオサイトに組み込みました。(※)
だいたい当初の思い通りに仕上がったので満足です。
https://github.com/kojimain/kojimain-portfolio
f:id:kojimainjp:20181227174246p:plain

追記:2018/12/29

本記事は無料枠で十分収まる範囲という想定ですが、Firestoreの指標の中に、趣味に収まらない速度で上昇する指標がありました。
一番ネックになっていた実装を修正しましたが、依然としてDoS攻撃を受けると一発でアウトな状況です。

そもそもFirebaseの使い方として、ログイン制アプリなどでリクエスト数が膨らまない、もしくは膨らんでも採算が取れるシチュエーションで使うのが正解なんだろうなという感想です。

以上の理由で、筆者のように公開サイトに搭載することはオススメしません。
現在Firestore以外の保管場所に移すことを検討中です。

(問題の指標↓)
f:id:kojimainjp:20181228231651p:plain