Raspberry Piとgo言語で部屋のコンディションを記録してグラフ化した (original) (raw)

この広告は、90日以上更新していないブログに表示しています。

部屋のコンディションをRaspberry Piとセンサー使って、3分ごとに記録してグラフ化するところまで出来た!
今留守にしているので、外気とちゃんと連動してるのが分かる。
これで自分の調子がいい時・悪い時の部屋のコンディションが調べられる☺️
もうすぐ二酸化炭素濃度センサーも届くので楽しみ💪 pic.twitter.com/WIoihOcZPw

— TAKUYA🐾個人開発で食うノウハウを書く (@craftzdog) September 3, 2018

自分のプロダクトばかり作っていると技術の幅も狭まってしまうので、定期的に趣味がてら題材を見つけて普段使わない技術に触れている。

自分にとってベストな部屋のコンディションが知りたい

今回は兼ねてからやりたかった、自分の部屋の温度や湿度などのコンディションを数分ごとに記録してグラフで可視化すること。 体調と空気の質は関連が深い。 気圧が低いと頭痛を起こす人もいるし、ジメジメしていると汗が気になって仕方がないという人もいるだろう。 そういう関係性を客観的に調べられたら、自分にとって最もパフォーマンスが出る条件がわかる。 まずはともあれ記録をとってグラフ化するところから始めようというわけだ。

使用機材

f:id:craftzdog:20180213103545j:plain

まだ届いていないけど、CO2センサーをAliExpressで買ったので、それも追々組み込む予定。

Go言語でセンサーデータを取得してCloud Firestoreに格納する

Go言語は前々から気になっていた言語。 やはりチュートリアルをやっただけではしっくりこないので、こういう実用的な題材で組むと一気に理解が進む。

Cloud Firestoreも気になっていたGoogleクラウド向けデータベース。 PouchDBみたいにオフライン同期に対応している点が面白い。 ちょっと複雑なクエリやComposite Indexingにも対応している。 まだBeta段階のようだけど、完成度は申し分なく、問題なく使える。

今回のプロジェクトを通して、この2つの技術と仲良くなりたい。

センサーデータの取得

ANAVIのセンサーキットにはサンプル集GitHubで公開されている。 これをそのまんま使って、実行結果をGolangによる簡単な文字列処理を経由してデータを取り出す。 例えばBMP180センサーのデータは以下のように取得する:

package sensors

import ( "log" "os/exec" "strings" "strconv" )

func GetTempAndPressure() (temperature float64, pressure float64, err error) { out, err := exec.Command("/home/pi/anavi-examples/sensors/BMP180/c/BMP180").Output() if err != nil { log.Fatal(err) }

s := string(out[:len(out)]) lines := strings.Split(s, "\n") lineTemp := lines[1] linePress := lines[2]

tempStr := strings.Split(lineTemp, ": ")[1] temperature, err = strconv.ParseFloat(tempStr[0:len(tempStr)-2], 32) pressStr := strings.Split(linePress, ": ")[1] pressure, err = strconv.ParseFloat(pressStr[0:len(pressStr)-4], 32)

return }

Firestoreへの格納

各センサーの取得スクリプトが書けたら、以下のようなmainスクリプトを書いてFirestoreに記録する:

package main

import ( "./sensors" "log" "time" "context" firebase "firebase.google.com/go" "google.golang.org/api/option" )

type RoomData struct { temperature float64 pressure float64 humidity float64 light float64 }

func recordData (roomData *RoomData) { ctx := context.Background() opt := option.WithCredentialsFile("<YOUR-SERVICE-ACCOUNT-KEY.json>") app, err := firebase.NewApp(ctx, nil, opt) client, err := app.Firestore(ctx) if err != nil { log.Fatal(err) } collection := client.Collection("conditions")

_, _, err = collection.Add(ctx, map[string]interface{}{ "createdAt": time.Now(), "humidity": roomData.humidity, "light": roomData.light, "pressure": roomData.pressure, "temperature": roomData.temperature, }) if err != nil { log.Fatalf("Failed adding alovelace: %v", err) } }

func main () { log.Println("Start capturing my room confitions")

temperature1, humidity, _ := sensors.GetTempAndHumid() log.Printf("Temperature: %f C\n", temperature1) log.Printf("Humidity: %f %%rh\n", humidity)

light, _ := sensors.GetLight() log.Printf("Light: %f Lux\n", light)

temperature2, pressure, _ := sensors.GetTempAndPressure() log.Printf("Temperature: %f C\n", temperature2) log.Printf("Pressure: %f %%rh\n", pressure)

data := RoomData{ temperature: temperature1, pressure: pressure, humidity: humidity, light: light, }

recordData(&data) log.Println("Finished recording data!") }

あとは3分ごとにこいつを実行するようにcronで設定すれば完成。 Firebaseのコンソールからデータが正しく記録されていることを確認する:

f:id:craftzdog:20180904154019p:plain

グラフ描画webフロントエンドを作る

デスクトップとモバイルのChromeで動けばよしとする。 最近はアロー関数が動くのでメソッドチェーンが書きやすい。 シンプルなプログラムなのでbabelもwebpackも使わず、Vanilla JSで行く。

ひとまず完成図がこちら:

f:id:craftzdog:20180904154436p:plain

とてもいいんじゃないでしょうか!

グラフはhighcharts.jsを使用。 Mixpanelとかでも使われているグラフ描画ライブラリ。 とても簡単にハイクオリティなグラフが書けるのでオススメ。 非商用利用なら無料。

ソースは以下のような感じ:

HTML:

Craftzdog's Room Conditions
<script src="lib/highcharts.js"></script>
<script src="lib/dark-unica.js"></script>
<script src="lib/moment.min.js"></script>
<script src="lib/moment-timezone-with-data-2012-2022.min.js"></script>

<script src="lib/firebase-app.js"></script>
<script src="lib/firebase-auth.js"></script>
<script src="lib/firebase-firestore.js"></script>

<link rel="stylesheet" href="lib/semantic.min.css" />
<link rel="stylesheet" href="lib/semantic-icon.css" />
<link rel="stylesheet" href="./app.css" />
<div class="ui container">
  <h1 class="align center">My Room Conditions <i class="home icon"></i></h1>
  <div class="ui stackable grid">
    <div id="temperature" class="eight wide column" style="width:100%; height:400px;"></div>
    <div id="humidity" class="eight wide column" style="width:100%; height:400px;"></div>
    <div id="pressure" class="eight wide column" style="width:100%; height:400px;"></div>
    <div id="light" class="eight wide column" style="width:100%; height:400px;"></div>
  </div>
</div>

<script src="./app.js"></script>
<script>
  retrieveData().then(data => {
    renderChart(data)
  })
</script>

JS:

var config = { apiKey: '', authDomain: '.firebaseapp.com', databaseURL: 'https://.firebaseio.com', projectId: '', storageBucket: '.appspot.com', messagingSenderId: '*' } firebase.initializeApp(config) var db = firebase.firestore() const settings = { timestampsInSnapshots: true } db.settings(settings) db.enablePersistence().catch(function(err) { if (err.code == 'failed-precondition') { } else if (err.code == 'unimplemented') { } console.error('Failed to enable persistence:', err) })

window.retrieveData = function() { var conditions = db.collection('conditions') return conditions .orderBy('createdAt', 'desc') .limit(300) .get() .then(querySnapshot => { var items = [] querySnapshot.forEach(doc => { items.push(doc.data()) }) items = items.map(item => { return Object.assign({}, item, { temperature: item.temperature - 1.4, humidity: item.humidity + 8.2 }) }) return items.reverse() }) }

window.renderChart = function(items) { const lastItem = items[items.length - 1]

const basicOptions = { time: { timezone: 'Asia/Tokyo' }, xAxis: { type: 'datetime' },

legend: {
  layout: 'vertical',
  align: 'right',
  verticalAlign: 'middle'
},

plotOptions: {
  series: {
    label: {
      connectorAllowed: false
    },
    pointStart: 2010
  }
},

responsive: {
  rules: [
    {
      condition: {
        maxWidth: 500
      },
      chartOptions: {
        legend: {
          layout: 'horizontal',
          align: 'center',
          verticalAlign: 'bottom'
        }
      }
    }
  ]
},

theme: {
  chart: {
    backgroundColor: {
      linearGradient: { x1: 0, x2: 1, y1: 0, y2: 1 },
      stops: [[0, '#2a2a2b'], [1, '#2a2a2b']]
    }
  }
}

}

Highcharts.theme.chart.backgroundColor = { linearGradient: { x1: 0, x2: 1, y1: 0, y2: 1 }, stops: [[0, '#2a2a2b'], [1, '#2a2a2b']] } Highcharts.setOptions(Highcharts.theme)

Highcharts.chart( 'temperature', Object.assign({}, basicOptions, { title: { useHTML: true, text: ' Temperature: ' + lastItem.temperature.toString().substr(0, 4) + ' ℃' }, yAxis: { title: { text: '℃' } }, series: [ { showInLegend: false, name: 'Temperature', data: items.map(item => [ item.createdAt.seconds * 1000, item.temperature ]) } ] }) )

Highcharts.chart( 'humidity', Object.assign({}, basicOptions, { title: { useHTML: true, text: ' Humidity: ' + lastItem.humidity.toString().substr(0, 4) + ' %rh' }, yAxis: { title: { text: '%rh' } }, series: [ { showInLegend: false, name: 'Humidity', data: items.map(item => [ item.createdAt.seconds * 1000, item.humidity ]) } ] }) )

Highcharts.chart( 'pressure', Object.assign({}, basicOptions, { title: { useHTML: true, text: ' Pressure: ' + Math.round(lastItem.pressure) + ' hPa' }, yAxis: { title: { text: 'hPa' } }, series: [ { showInLegend: false, name: 'Pressure', data: items.map(item => [ item.createdAt.seconds * 1000, item.pressure ]) } ] }) )

Highcharts.chart( 'light', Object.assign({}, basicOptions, { title: { useHTML: true, text: ' Light: ' + lastItem.light + ' lux' }, yAxis: { title: { text: 'Lux' } }, series: [ { showInLegend: false, name: 'Light', data: items.map(item => [item.createdAt.seconds * 1000, item.light]) } ] }) ) }

Future Work

今後はこれらのデータをもとにして、

などを追加していきたい。