部屋の湿度を測りたくてDHT20を買った.
さて,こいつで計測した湿度を,最終的にはGrafanaで見られるようにしたい.
アーキテクチャを考える
DHT20はただのセンサーであって,こいつの値を送信・受信するためには別のサーバが必要になる.ただ,センサーから定期的に湿度を取って送るだけなら,大したCPUは必要ないので,Raspberry Pi Zero WHを買った.
こいつにDHT20を接続してPi Zeroで値を取る.で,Pi Zero上でPrometheus Exporterを動かしておいて,Kubernetes上のPrometheusから取りに行くのはどうだろうか.そうすれば,Prometheusでデータが取れたあとはすべてサーバ側での処理になる.
とりあえず湿度を取れる状態にする
DHT20にGroveコネクタが付属しているので,
https://akizukidenshi.com/goodsaffix/DHT20.pdf
どこに接続するかはデータシートを見る.
Raspberry PiはまぁいつもどおりにOSをセットアップしておく.
で,プログラムをどうするかという問題までくる.DHT20はI2Cというインターフェースを使うが,これを扱うものとして例示されているのはPythonが多い.しかし,Raspberry Pi Zero WH上でPythonを動かしたくない. さらに,Pi Zeroは
https://elinux.org/RPi_Hardware
これによると32bitのARMv6のCPUである.つまりx86_64上でコンパイルするのがめんどくさい.こういときはGoに限る.
というわけで,巷に流れるPythonやCのプログラムを参考にして,I2CをGoで扱ってみる.
package main import ( "log" "math" "time" "github.com/d2r2/go-i2c" ) func main() { i2c, err := i2c.NewI2C(0x38, 1) if err != nil { log.Fatal(err) } // Free I2C connection on exit defer i2c.Close() // Need to wait 100ms after launched time.Sleep(100 * time.Millisecond) var initial byte = 0x71 ret, err := i2c.ReadRegU8(initial) if err != nil { log.Fatal(err) } // ret should be 28(0x1c) log.Printf("init code %d", ret) time.Sleep(10 * time.Millisecond) // Start measure _, err = i2c.WriteBytes([]byte{0x00, 0xAC, 0x33, 0x00}) if err != nil { log.Fatal(err) } // Need to wait after sending ac3300 time.Sleep(80 * time.Millisecond) dat := make([]byte, 7) _, err = i2c.ReadBytes(dat) if err != nil { log.Fatal(err) } // byte is uint8 (8bits), it is not enough to shift // So cast []uint8 to []uint32 var long_dat []uint32 for _, d := range dat { long_dat = append(long_dat, uint32(d)) } // Get humidity and tempreature data hum := long_dat[1]<<12 | long_dat[2]<<4 | ((long_dat[3] & 0xF0) >> 4) tmp := ((long_dat[3] & 0x0F) << 16) | long_dat[4]<<8 | long_dat[5] // Calcurate real data real_hum := float64(hum) / math.Pow(2, 20) * 100 real_tmp := float64(tmp) / math.Pow(2, 20)*200 - 50 log.Printf("hum: %f", real_hum) log.Printf("tmp: %f", real_tmp) }
データシートに一応書いてあるんだけど,かなりわかりにくかった.まず,I2Cで新規接続するときには 0x38
を指定する.ここがI2Cデバイスのアドレスになっている.そして初期化は 0x71
を送る.そうすると初期化コードが帰ってくるのだが,ここは正直全然わからん.
If the status word and 0x18 are not equal to 0x18, initialize the 0x1B, 0x1C, 0x1E registers, details Please refer to our official website routine for the initialization process; if they are equal, proceed to the next step
なので, 0x18
が正常なのだが,たまに 0x1c
が帰ってくることがある.これが何を意味していて何を必要としているのか,どこを見てもわからんかった.ちなみに 0x1c
でもデータは普通に取れてた.
で,測定開始は 0xac
, 0x33
, 0x00
を送る.
湿度データが入っているのが,2, 3, 4バイトめの上位4ビット.温度データが4バイトめの下位4ビットと5バイト,6バイトめ.ビットシフトするとuint8だとビット数が全然足らないのでuint32にデータを格納している.
このプログラムを
$ GOOS=linux GOARCH=arm GOARM=6 go build
すると,Pi Zeroで動くバイナリが生成できるので,こいつをデバイスに送信して動かす.
Prometheus exporterにする
github.com/prometheus/client_golang を使えばだいたいかんたんに作れる.これで30秒おきに上記のプログラムを実行して取れたデータをPrometheus exporterに乗せておく.
再起動の管理とかログの管理とかlogrotateとかがめんどくさいので,こいつをsystemdで動かす.
[Unit] Description=Humidity and Tempreature exporter for Prometheus After=network.target [Service] Type=simple User=root Group=root WorkingDirectory=/opt/prometheus-humidity-exporter ExecStart=/usr/bin/prometheus-humidity-exporter Restart=always RestartSec=10 Environment=LANG=en_US.UTF-8 SyslogIdentifier=prometheus-humidity-exporter RemainAfterExit=no [Install] WantedBy=multi-user.target
で,あとはPi ZeroのIPをターゲットにして,Kubernetesクラスタ上のPrometheusから値を読ませる.そしたらもうGrafanaで見えるようになる.
ふはははは.