DHT20で湿度計測してprometheus exporterで出力しGrafanaで可視化する

部屋の湿度を測りたくてDHT20を買った.

akizukidenshi.com

さて,こいつで計測した湿度を,最終的にはGrafanaで見られるようにしたい.

アーキテクチャを考える

DHT20はただのセンサーであって,こいつの値を送信・受信するためには別のサーバが必要になる.ただ,センサーから定期的に湿度を取って送るだけなら,大したCPUは必要ないので,Raspberry Pi Zero WHを買った.

akizukidenshi.com

こいつに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

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で見えるようになる.

ふはははは.

参考

craft-gogo.com

s-design-tokyo.com

hatakekara.com