フューチャー技術ブログ

GoでADコンバータ読み出し~観葉植物監視bot構築~

eyecatch.png

夏休み自由研究ブログ連載の8本目です。

はじめに

こんにちは。TIG/DXユニットの宮永です。

昨今の観葉植物ブームに便乗して先日ドラセナ・コンパクタという熱帯アフリカ原産の植物を購入しました。茎の縞模様とパイナップルを彷彿させるような葉っぱがとても可愛らしい植物です。ふとしたときに眺めるととても癒やされます。

観葉植物を購入してから気づいたのですが、水やりというのは結構大変です。植物を置いている場所の室温、日当たり、湿度などから適切な水分量を見極めるのが素人の私には難しいと感じています。

また、生来がズボラな性格をしているため、水をやるのを忘れてしまったりなども今後考えられます。

枯れてしまってから泣きをみることがないように、今回は土壌水分計とRaspberryPi、ADコンバータを使用して観葉植物の水分量を監視するSlackBotを作成します。

また、本記事のサンプルコードはorangekame3/mcp3002にて公開していますので、参考にしてください。

システム構成

構成はシンプルです。

RaspberryPiにこちらの土壌水分計を取り付け、ADコンバータMCP3002で読み出しを行います。読み出した電圧値に応じてSlackBot経由で通知を行います。

Pythonで同様のことを行っている記事はたくさんありますが、Go言語での実装例は殆どなかったのでその点で参考になれば幸いです。

使用機器

配線

余談ですが、RaspberryPiにはpinoutというコマンドが標準で備わっています。配線するときに便利ですのでおすすめです。

image.png

せっかくですので、pinoutで出力されたピン番号に沿って今回の配線を説明します。

J8:
3V3 (1) (2) 5V
GPIO2 (3) (4) 5V
GPIO3 (5) (6) GND
GPIO4 (7) (8) GPIO14
GND (9) (10) GPIO15
GPIO17 (11) (12) GPIO18
GPIO27 (13) (14) GND
GPIO22 (15) (16) GPIO23
3V3 (17) (18) GPIO24
GPIO10 (19) (20) GND
GPIO9 (21) (22) GPIO25
GPIO11 (23) (24) GPIO8
GND (25) (26) GPIO7
GPIO0 (27) (28) GPIO1
GPIO5 (29) (30) GND
GPIO6 (31) (32) GPIO12
GPIO13 (33) (34) GND
GPIO19 (35) (36) GPIO16
GPIO26 (37) (38) GPIO20
GND (39) (40) GPIO21

ADコンバータは秋月商店で購入しました。

I-02584.jpg

商品詳細ページにデータシートがあったため、こちらを使用して配線の説明をします。

MCP3002には前後があります。写真にあるように前方には半円のマークがついています。
下図のように半円の印がついたものを上向きにした場合、左上から1,2,3…と番号が振られます。

image.png

以下、MCP3002とRaspberryPiそして土壌水分計との接続関係です。
今回はMCP3002のCH0のみ使用したため、CH1にはなにも接続していません。

▼配線一覧

MCP3002ピン番号 RaspberryPiピン番号 土壌水分計
1(CS/SHDN) 24(GPIO8) Vcc
2(CH0) - Aout
3(CH1) - -
4(Vss) 6(GND) GND
5(Din) 19(GPIO10) -
6(Dout) 21(GPIO9) -
7(CLK) 23(GPIO11) -
8(Vdd/Vref) 1(3V3) -

実装

今回実装するにあたってこちらgolang.org/x/exp/io/spiパッケージを使用しました。

また、MCP3002からの読み出しの実装についてはこちらのリポジトリでMCP3004、MCP3008、MCP3204、MCP3208の実装がされていたため、MCP3002のデータシートと比較しながら実装の参考とさせていただきました。

デバイスの読み込み

まずはgolang.org/x/exp/io/spiを使用して、デバイスを読み込みます。

main.go
dev, err := spi.Open(&spi.Devfs{
Dev: "/dev/spidev0.0",
Mode: spi.Mode0,
MaxSpeed: 3600000,
})
if err != nil {
fmt.Println(err)
}
defer dev.Close()

MCP3002のドライバ作成

デバイスを読み込んだらMCP3002の仕様に合わせてドライバを作成します。

mcp3002.go
package mcp3002

import (
"fmt"

"golang.org/x/exp/io/spi"
)

type MCP3002 struct {
Dev *spi.Device
Vref float64
Channel int
}

func (m MCP3002) Read() (float64, error) {
if m.Channel < 0 || m.Channel > 1 { // ---------------- (1)
return 0, fmt.Errorf("channel is only selected 0 or 1")
}
cmd := byte(0x68) // ------------------------------------- (2)
if m.Channel == 1 {
cmd = 0x78
}
in := []byte{cmd, 0x00}
out := make([]byte, 2)
if err := m.Dev.Tx(in, out); err != nil {
return 0, fmt.Errorf("failed to read channel %d,%w", m.Channel, err)
}
data := int(out[0]&3)<<8 | int(out[1]) // ------------- (3)
return (m.Vref / 1024) * float64(data), nil
}

上記の実装で初見でよくわからんと思うポイントにナンバリングしました。順に説明します。

(1)MCP3002のチャネル数

(1)の実装はMCP3002のチャネル数に対するバインディングです。冒頭で説明したとおり、MCP3002はチャネルが2つあります。使用するチャネルによってMCP3002に渡すバイナリも変わるため、ここのハンドリングは大事です。

(2)MCP3002に渡すバイナリ

MCP3002のデータシートを参照すると以下のようなシーケンス図が記載されています。DinがRaspberryPi→MCP3002にわたすバイナリ、DoutがMCP3002→Raspberryで受け取るバイナリです。

image.png

データシートにはMCP3002に送るべきバイナリと読みとるべきバイナリについて記載がありました。
このデータシートによれば、MCP3002に向けて2バイト渡せば良いことがわかります。そのうち、1バイト目には条件に沿ったバイナリを渡してあげる必要があります。

image.png

StartBitの次に記載されているSGL/DIFF、ODD/SIGN、について0/1のどちらを渡すかは使用するチャネルによります。
以下の図に従えばCH0を使用する場合は10を渡せば良いことがわかります。MSBFについてですが、Figure5-1に従う読み出しを行う場合は1にします。よってRaspberryPi→MCP3002にむけて送信するべきバイナリは

01101000 00000000

であることがわかります。これは16進数表記で0x68です。同様にして使用しているチャネルが1である場合SGL/DIFF、ODD/SIGNが11となるためRaspberryPi→MCP3002にむけて送信するべきバイナリは

01111000 00000000

となります。これは16進数表記で0x78です。これで(2)の謎の16進数の役割がおわかりいただけたかと思います。

image.png

(3)10ビット読みだし

データシートのFIGURE6-1に記載の通り、末尾10ビットに読み出すべき情報が含まれています。RaspberryPi→MCP3002で2バイト渡したように、MCP3002→RaspberryPiでも2バイト受け取ります。

1バイト目と3で論理積を取ることで1バイト目の下方2ビットの値を特定することができます。

例)
10110101と00000011(3)の論理積 は00000001です。

上記で算出したバイナリを8ビット左シフトし後方1バイトと論理和を取ることで、読み出すべき10ビットを取得することができます。

例)
00000001を8ビット左シフトすると00000001 00000000になります。

MCP3002→RaspberryPiで受け取った2バイト目のバイナリが01011010だとすると、上記00000001 0000000001011010の論理和は

00000001 01011010

であることがわかります。これで(3)の謎の演算がわかったかと思います。
あとはこの読み出した値に対して閾値を設定してやれば、観葉植物監視botの要件を達成できます。

呼び出し元の実装

1つのADコンバータに2つのチャネルがついているので監視する観葉植物も2つ登録できます。
Plant構造体を作成してADコンバータを登録するようにします。
閾値0.4を境にbotが通知する状態を変えます。

main.go
package main

import (
"fmt"
"github.com/mcp3002"
"golang.org/x/exp/io/spi"
)

const (
Tirsty = "喉乾いたよ〜"
Moist = "お水はもう十分だよ"
)

type Plant struct {
ADC mcp3002.MCP3002
Status string
}

func (p Plant) addState(v float64) Plant {
resp := p
if v > 0.4 {
resp.Status = Tirsty
return resp
}
resp.Status = Moist
return resp
}

func (p Plant) addChannel(ch int) Plant {
resp := p
resp.ADC.Channel = 0
if ch == 1 {
resp.ADC.Channel = 1
return resp
}
return resp
}

func (p Plant) addADC(mcp mcp3002.MCP3002) Plant {
resp := p
resp.ADC = mcp
return resp
}

上記実装にSlack通知機能を実装します。cronで定期的に土壌水分を監視してPlant.StatusがThirstyである場合はSlackで通知するようにします。Slack通知にはslack-goをcronにはrobfig/cronを使用しました。

main.go
package main

import (
"fmt"
"log"
"os"
"time"

"github.com/joho/godotenv"
"github.com/mcp3002"
"github.com/robfig/cron/v3"
"github.com/slack-go/slack"
"golang.org/x/exp/io/spi"
)

func main() {
dev, err := spi.Open(&spi.Devfs{
Dev: "/dev/spidev0.0",
Mode: spi.Mode0,
MaxSpeed: 3600000,
})
if err != nil {
fmt.Println(err)
}

defer dev.Close()

// AD converの設定
mcp := mcp3002.MCP3002{
Dev: dev,
Vref: 3.3,
}

// slackの設定ファイル読み込み
err = godotenv.Load("../.env")
if err != nil {
log.Fatalf("read slack env", err)
}
tkn := os.Getenv("TOKEN")
client := slack.New(tkn)

var plantA Plant
plantA = plantA.addADC(mcp)
plantA = plantA.addChannel(0)

c := cron.New()
c.AddFunc("@every 1s", func() {
if err := worker(plantA, client); err != nil {
log.Fatal(err)
}
})
c.Start()
for {
time.Sleep(time.Second)
}

}

func worker(p Plant, client *slack.Client) error {
v, _ := p.ADC.Read()
p = p.addState(v)
if p.Status == Tirsty {
_, _, err := client.PostMessage("#home", slack.MsgOptionText(p.Status, true))
if err != nil {
return fmt.Errorf("failed to post message %w", err)
}
}
return nil
}

const (
Tirsty = "喉乾いたよ〜"
Moist = "お水はもう十分だよ"
)

type Plant struct {
ADC mcp3002.MCP3002
Status string
}

func (p Plant) addState(v float64) Plant {
resp := p
if v > 0.4 {
resp.Status = Tirsty
return resp
}
resp.Status = Moist
return resp
}

func (p Plant) addChannel(ch int) Plant {
resp := p
resp.ADC.Channel = 0
if ch == 1 {
resp.ADC.Channel = 1
return resp
}
return resp
}

func (p Plant) addADC(mcp mcp3002.MCP3002) Plant {
resp := p
resp.ADC = mcp
return resp
}

それでは最後に上記ジョブを実行してみます。

eyecatch.png

ドラセナが喉の乾きを訴えかけてきました…

さいごに

今回は土壌水分計とADコンバータを用いて観葉植物監視botを作成しました。
実はgolang.org/x/exp/io/spiパッケージはすでにメンテがされていないようで、https://github.com/periphの利用が現在は推奨されているようです。

記事をほぼ書き終えてから気づいたので少し反省です。今後GoでRaspberryPi周りを触る際はhttps://github.com/periphを利用してみようとおもいます。Gobotもこちらをベースに実装を行っているようです。

最後まで読んでいただきありがとうございました。