Raspberry PiとGo言語でミニトマトの栽培環境を監視してLINE Botで通知する

TOP

こんにちは。ビジネスソリューション開発グループ チャネルソリューションチームの長谷川と申します。

家庭菜園としてミニトマトの栽培を始めました。

せっかくの機会なので、温度、湿度と土の水分量をRaspberry Piで監視し、LINE Botを通じてLINEのタイムラインに通知する仕組みを作りました。

使ったもの

Raspberry Pi2 Model B

Amazonで¥4,000前後です。

GW-USNANO2A

プラネックス製の無線LAN子機。Raspbery Pi2は無線LANが搭載されていないため、USBタイプの子機が必要となります。Amazonで¥990くらいです。

Soil Moisture Sensor

アナログの土壌の湿度センサーです。 秋月電子通商で¥500。

MCP3008

上記のアナログセンサーをデジタルに変換するためのA/Dコンバータです。 秋月電子通商で¥220。

BME280

i2cを使用した温度差・気圧センサーです。 秋月電子通商で¥1,080。

外箱

密閉型ですと温度センサーに影響を与えてしまうので、ある程度隙間の空いた木箱を外箱にしました。 百均で¥108。

これらを使い、ミニトマトの植えた土の土壌湿度および外気温・湿度・気圧を監視し、AWS上のDynamoDBに定期的に状態を送信し、LINE Botとして会話するプログラムを作成します。

※ 価格はすべて2017年6月のものです。

事前準備

LINE Bot のwebhook機能を使用するためにはグローバルからのHTTPS接続を許可しておく必要があります。 自宅のインターネットプロバイダに確認し、固定IPを取得するかダイナミックDNS(DDNS)サービスを使用してURLベースでRaspberry Piに接続できるようにしておいてください。

アカウント設定

下記のアカウント登録をしておきます。

  • LINE Developer Trial
  • AWS

LINE Developer TrialはLINE Bot, AWSはDynamoDB使用のために必要となります。

Wi-Fiネットワークの接続

下記を参考にGW-USNANO2AをRaspberry Piに接続し、自宅のWi-Fiネットワークに参加させます。

■ 参考
raspberry pi2 で 無線 LAN 設定まで
$ sudo sh -c 'wpa_passphrase [Wi-FiのSSID] [パスフレーズ] >> /etc/wpa_supplicant/wpa_supplicant.conf'
$ sudo /etc/init.d/networking restart

ifconfigでwlan0にIPが割り当てられていることを確認します。

$ ifconfig
wlan0     Link encap:Ethernet  HWaddr xx:xx:xx:xx:xx:xx
          inet addr:192.168.0.2  Bcast:192.168.0.255  Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:101017 errors:0 dropped:1127 overruns:0 frame:0
          TX packets:66969 errors:0 dropped:1 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:23361109 (22.2 MiB)  TX bytes:24062827 (22.9 MiB)

センサー接続

購入したセンサーをRaspberry Piに接続します。

回路図は以下のようになります。

f:id:g-editor:20170711112617j:plain

Soil Moisture Sensorはアナログ信号のため、A/DコンバータMCP3008を使用してデジタル信号に変換します。 A/Dコンバータは12bitのMCP3208のほうが精度が高いのですが、手元にあったのがMCP3008だったのでこちらで実装しました。

MCP3008はSPIを使用するため、raspi-config を開いてSPIをenableに変更します。

# raspi-config

f:id:g-editor:20170711112648p:plain 9 Advanced Optionsを選択します。

f:id:g-editor:20170711112659p:plain A6 SPIを選択します。

f:id:g-editor:20170711112710p:plain Yes を選び有効化します。

MCP3008をRaspberry Piと接続します。

MCP3008Raspberry Pi
VDD+3.3V
VREF+3.3V
AGNDGND
CLKGPIO 11 (SCLK)
DOUTGPIO 9 (MISO)
DINGPIO 10 (MOSI)
CS/SHDNGPIP 8 (CE0)
DGNDGND

次にBME280を接続します。 BME280はRaspberry Pi との通信にi2cを使用するためMCP3008同様raspi-configからi2cを有効化します。

i2cもSPI同様、rasp-configからONにします。

f:id:g-editor:20170711112723p:plain 9 Advanced Optionsを選択します。

f:id:g-editor:20170711112734p:plain A7 I2Cを選択します。

f:id:g-editor:20170711112749p:plain Yes を選び有効化します。

BME280のマニュアルに従い、j3にはんだを流し込みi2c接続を有効化します。

※マニュアルを見たい方は秋月電子通商のWebサイトにて通販コード「K-09421」を入力し、該当した商品ページから確認してください。

BME280とRaspberry Piは以下の配線で接続します。

BME280Raspberry Pi
VDD+3.3V
GNDGND
SDIGPIO2 (SDA)
SDOGND
SCKGPIO3 (SCL)

Let's EncryptでSSL証明書を発行する

LINE Botのwebhook用URLはhttpsである必要があります。今回はLet's Encryptを使用しました。DDNSを使用している場合、DDNSがmydns.jpだと同一ドメイン上限に引っかかってエラーになる可能性があります(1週間に20サブドメインらしい)。

Let's EncryptのSSL証明書発行にはcertbotという専用ツールがありますので、そちらをダウンロードし証明書を発行します。

$ git clone https://github.com/certbot/certbot
$ cd certbot
$ sudo  ./certbot-auto certonly --standalone -d 自サーバのドメイン -m 連絡先メールアドレス --agree-tos -n

Le'ts Encryptによる証明書発行時、認証局から発行先サーバへhttpによるアクセスが必要となります。あらかじめgolangでエコーサーバ用スクリプトを作成し、rootで起動しておきましょう。

echo.go
package main

import (
  "fmt"
  "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "I'm Here.")
}

func main() {
  http.HandleFunc("/", handler)
  http.ListenAndServe(":80", nil)
}

正常に発行されると下記ディレクトリに証明書がインストールされます。

/etc/letsencrypt/

/etc/letsencrypt/archive/自サーバのドメイン/ 配下のfullchain1.pem,privkey1.pemを使用しますので正常に生成されていることを確認します。

/etc/letsencrypt/archive/自サーバのドメイン/
├── fullchain1.pem
└── privkey1.pem

LINE Developer Trial申請

Raspberry Pi上のWEBサーバにSSLでアクセスできるようになったら、LINE Developer Trialの申請を行います。

■申請ページ
https://business.line.me/ja/services/bot

Developer Trial として登録しないとPUSHメッセージが使用できないので間違えないようにしましょう。

実装

実装はGo言語で行います。Goは各種センサーに接続するライブラリが揃っていますし、並列処理により複数のセンサーにアクセスでき、かつシングルバイナリなので簡単に複数のサーバにプログラムを配布できるため個人的にはIoTと相性のよい言語だと思っています。

パッケージ

下記のパッケージをインポートします。

github.com/line/line-bot-sdk-go/linebot
golang.org/x/exp/io/i2c
golang.org/x/exp/io/spi
github.com/quhar/bme280
github.com/advancedclimatesystems/io/spi/microchip
github.com/advancedclimatesystems/io/adc
github.com/aws/aws-sdk-go/aws
github.com/aws/aws-sdk-go/aws/session
github.com/aws/aws-sdk-go/service/dynamodb
github.com/robfig/cron

github.com/line/line-bot-sdk-go/linebotはLINE Bot用、golang.org/x/exp/io/i2c,golang.org/x/exp/io/spiはそれぞれi2c,spiへアクセスするためのパッケージです。

MCP3008からのデータを取得するため、github.com/advancedclimatesystems/io/spi/microchip,github.com/advancedclimatesystems/io/adcをインポートします。

BME280からの数値取得はGoBotでも可能なようですが、私の環境ではうまく動きませんでしたのでgithub.com/quhar/bme280を使ってセンサーから温度・気圧・湿度を取得しました。

github.com/aws/aws-sdk-go/はGoからAWSへの各種サービスを使用するためのSDKとなります。

github.com/robfig/cronはcron表記で決まった時刻や間隔で特定の処理を実行するためのパッケージです。

プログラム

Let's EncryptやLINEのトークンなどはまとめてconst設定しておきます。

const (
    LetsencryptChain   = "/etc/letsencrypt/archive/yourdomain/fullchain1.pem"
    LetsencryptPrivate = "/etc/letsencrypt/archive/yourdomain/privkey1.pem"
    SSLPort            = ":443"
    ChannelId          = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    Secret             = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    Token              = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
)

aws-sdk-goからawsの各種サービスを使用するために~/.aws/credentialsに認証情報を記述します。

[default]
aws_access_key_id = XXXXXXXXXX
aws_secret_access_key = XXXXXXXXXXXX

HTTPSサーバの起動

https接続を待ち受けるためにhttp.ListenAndServeTLSでWEBサーバを起動します。Goでは一般ユーザーでウェルノウンポートでhttpが起動できないので、root権限で実行する必要があります。

err := http.ListenAndServeTLS(SSLPort, LetsencryptChain, LetsencryptPrivate, nil)
if err != nil {
    log.Fatal(err)
}

BME280から温度・気圧・湿度を取得

BME280から値を取得するために、i2cをオープンします。

dev, err := i2c.Open(&i2c.Devfs{Dev: "/dev/i2c-1"}, 0x76)

&i2c.Devfs に渡すデバイスファイル、アドレスは各自異なりますので i2cdetect で確認しておきます。

BME280 からの値の取得は bme280.EnvData() を使用します。

bme := bme280.New(dev)
bme.Init()
t, p, h, err := bme.EnvData()

正常に取得できると左から順番に温度、気圧、湿度、エラーが返却されます。

Soil Moisture Sensor と MCP3008 から土の湿度を取得

A/Dコンバータと接続するためにSPIポートを開きます。

conn, err := spi.Open(&spi.Devfs{
    Dev:      "/dev/spidev0.0",
    Mode:     spi.Mode0,
    MaxSpeed: 3600000,
})

オープンしたコネクションをMCP3008に渡します。

adc = microchip.MCP3008{
    Conn:      conn,
    Vref:      5.0,
    InputType: adc.PseudoDifferential,
}

MCP3008.OutputCode(0)でローデータの取得ができます。

hum, err := adc.OutputCode(0)

同様にMCP3008.Voltage(0)でセンサー間を流れる電圧を測定できます。

v, err := adc.Voltage(0)

Raspberry Piからプッシュ通知

linebot.New() しLINE Botのインスタンスを取得します。

bot, err = linebot.New(Secret, Token)

linebot.PushMessage()を使用するとPUSHメッセージを送信することが出来ます。

var messages []linebot.Message
message := linebot.NewTextMessage("Soil humidity is less.")
messages = append(messages, message)
bot.PushMessage(ChannelId, messages...).Do()

リプライメッセージ

LINE Botからのcallbackを受けるHandleFuncを設定します。

http.HandleFunc("/callback", callbackMessage)

callbackからメッセージをリプライする関数を作成します。

linebot.ParseRequest(*http.Request)でBotのリクエストをパースし、linebot.ReplyMessage().Do()で、送信メッセージに応じた返信を作成します。

func callbackMessage(w http.ResponseWriter, r *http.Request) {
    events, err := bot.ParseRequest(r)
    if err != nil {
        if err == linebot.ErrInvalidSignature {
            fmt.Println(err)
        }
        return
    }

    for _, event := range events {
        if event.Type == linebot.EventTypeMessage {
            switch message := event.Message.(type) {
            case *linebot.TextMessage:
                if _, err = bot.ReplyMessage(event.ReplyToken, linebot.NewTextMessage("Hello!")).Do(); err != nil {
                    log.Print(err)
                }
            }
        }
    }
}

最後に、取得したセンサーのデータを定期的にdynamodbへ送信するための関数を用意します。

dynamo := dynamodb.New(session.New(), aws.NewConfig().WithRegion("ap-northeast-1"))
func pushGaugeData(t float64, p float64, h float64, c int, v float64) {
    now := time.Now()
    param := &dynamodb.UpdateItemInput{
        TableName: aws.String("DynamoDBTable"),
        Key: map[string]*dynamodb.AttributeValue{
            "id": {
                N: aws.String(fmt.Sprint(now.Unix())),
            },
        },
        ExpressionAttributeNames: map[string]*string{
            "#temperature": aws.String("temperature"),
            "#pressure":    aws.String("pressure"),
            "#hydro":       aws.String("hydro"),
            "#volt":        aws.String("volt"),
            "#raw":         aws.String("raw"),
        },
        ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
            ":temperature": {
                N: aws.String(strconv.FormatFloat(t, 'f', 2, 64)),
            },
            ":pressure": {
                N: aws.String(strconv.FormatFloat(p, 'f', 2, 64)),
            },
            ":hydro": {
                N: aws.String(strconv.FormatFloat(h, 'f', 2, 64)),
            },
            ":volt": {
                N: aws.String(strconv.FormatFloat(v, 'f', 2, 64)),
            },
            ":raw": {
                N: aws.String(fmt.Sprint(c)),
            },
        },
        UpdateExpression: aws.String("set #temperature = :temperature, #pressure = :pressure, #hydro = :hydro, #volt = :volt, #raw = :raw"),

        ReturnConsumedCapacity:      aws.String("NONE"),
        ReturnItemCollectionMetrics: aws.String("NONE"),
        ReturnValues:                aws.String("NONE"),
    }
    dynamo.UpdateItem(param)
}

上記の関数をcronで定期的に実行するように設定します。cronは通常のcron記法と異なり秒まで設定するので気を付けましょう。

c := cron.New()
c.AddFunc("0 0 6 * * *", func() { fmt.Println("Wake me up!") })
c.Start()

キャラクター申請

ここまでできたらあとはgo buildしてBotを起動するだけになりますが、幸いなことに私の住む埼玉県北本市にはとまちゃんというトマトをモチーフにしたイメージキャラクターがおり、とまちゃんは使用申請を出し認証を受けることで誰でも自由に使うことが出来ます。

せっかくミニトマトを栽培するので、Botのキャラクターにとまちゃんを使わせていただくことにします。

申請書は北本市のホームページからダウンロードできますので、必要事項を記入して指定のアドレスにメールすると5日前後で可否通知が郵送されます。

実行

Goのhttpは、一般ユーザーでは443番をbindできないためrootで実行します。

正常に起動されるとLINE Botから通知が届きます。 f:id:g-editor:20170711112830p:plain

LINE Botと友だちになりメッセージを送るとRaspberry Piにwebhookされ、そのタイミングでセンサーから値を取得して、値に応じてランダムな応答メッセージを返却します。 f:id:g-editor:20170711112857p:plain

さらに詳しく知りたい場合はBotに「詳しく」を聞くと、現在の取得値を返信します。 f:id:g-editor:20170711112916p:plain

またトマトの栽培においては水はやりすぎず適度に土が乾いた状態が良いとのことなので、定期的にSoil Moisture Sensor 経由でraw値を取得し、raw値が500の場合に「お水がほしい」というメッセージを返却するようにしました。 f:id:g-editor:20170711112924j:plain

この後私が奥さんにメールし、娘と水やりをするフローとなります。

気を付けること

Soil Moisture Sensor、MCP3008、BME280を使って取得する各種の値は誤差が大きいので、センサーの値だけに頼らず定期的に状態を確認しましょう。

なお、この記事で解説したプログラムのソースはGitHubに公開しております。

gnavi-blog/tomato


長谷川
ぐるなびビッグデータを使った商品開発を行っています。 2児の父親で、プライベートでは子供が喜ぶような何かを作ったりしてますが、どれも反応がいまいちなのが悲しいです。