出窓菜園の自動給水と、土壌湿度データの可視化システムを作った

#概要

　出窓ガーデニング　＝　出窓菜園ティストのまえぷーです。

　普段は[note](https://note.com/kmaepu/m/mf49c2a0a2438)にマガジンとして掲載しておりますが、技術的なところの記事を書きたいと思いQiitaに投稿した次第です。

　紆余曲折して給水システムの自動化と菜園管理のためにデータ収集システムを作りました。
この記事では、本システムの作り方を備忘録として書いています。
　最後にソースコードを載せていますが、時間処理を適当に作っております。ちゃんと作ろうとしている方は時間処理以外をご参考にしていただければ幸いです。

　できたのものはこちら。
【給水の様子】
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">自動給水システムの試運転。<br>この後びしょ濡れになりました(笑) <a href="https://t.co/EnmYwafViQ">pic.twitter.com/EnmYwafViQ</a></p>&mdash; まえぷー@出窓菜園 BWG (@kmaepu) <a href="https://twitter.com/kmaepu/status/1257153357346533376?ref_src=twsrc%5Etfw">May 4, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

【データの可視化】
![ambient.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183282/928b7785-7ecb-3007-727c-4b450568c2e6.jpeg)


#システム構成

　本システムでは、土壌湿度センサを使用して土の乾き具合を計測し、乾いているとマイコンが判断したら給水ポンプの電源をONにして給水します。
　と同時に、取得した土壌湿度データを[Ambient](https://ambidata.io/)でグラフ化しています。

![出窓菜園　自動給水システム.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183282/86ea2f28-99e4-0bf0-c2f2-7054932ee173.jpeg)


#解説
　
##ポンプの駆動
　ポンプの電源をリレーでON/OFF制御することにより、給水を制御しています。

　リレーは[こちら](https://www.switch-science.com/catalog/807/)を使用しています。ポンプはUSB給電タイプの[こちら](https://www.amazon.co.jp/dp/B01FU0ZBN0?tag=note0e2a-22&linkCode=ogi&th=1&psc=1)
を使用しています。

　なぜUSB給電にしたかたいうと、デモを持ち運ぶときにモバイルバッテリからの給電をしたかったためです。備え付けのシステムならば通常のコンセントプラグでも問題ないですね。

　USB給電用の治具を次のように製作しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183282/1c8dd5dd-ba12-07e2-4a84-f8a7c8baed13.png)

##土壌湿度の取得
　土が乾いてきたかをセンシングするために、土壌湿度センサ**[Gravity: Analog Soil Moisture Sensor For Arduino](https://www.dfrobot.com/product-599.html?search=moisture&description=true)**を使用しました。

　このセンサはアナログデータ（電圧）で土壌水分量が出力されます。　電源電圧5Vの時に出力電圧は0～4.2[V]のようです。サンプルコードから読み解くと、次のように乾き具合を３段階でわかるようです。（デジタル値）今回は300を閾値にして、以下になると給水を4秒間行うようにしました。

```
　# 0  ~300     dry soil
  # 300~700     humid soil
  # 700~950     in water
```

　デジタル値から土壌水分量に換算する式が理解できなかったので、今回は割愛しています。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">よし。土場湿度をM5Stackで取得できるようになったぞ！<a href="https://twitter.com/hashtag/M5Stack?src=hash&amp;ref_src=twsrc%5Etfw">#M5Stack</a> <a href="https://t.co/ctZhDJxZTH">pic.twitter.com/ctZhDJxZTH</a></p>&mdash; まえぷー@出窓菜園 BWG (@kmaepu) <a href="https://twitter.com/kmaepu/status/1256872543476051970?ref_src=twsrc%5Etfw">May 3, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


##データをアップロード
　MQTTでいったんラズパイ上のNode-REDに送り、そこからAmbientへアップロードしています。

![20200504_102902.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183282/6b546e25-84b3-91bd-90c7-b4e35e51a8e2.jpeg)

　Node-REDのフローは次のようになっています。MQTTで受け取ったデータをjson形式に直してAmabientへ送信しています。

　一見無駄な処理に見えますが、今後APIとつなげていこうと思うので拡張性を考えてこのようにしています。Node-REDのフローは次の通りです。受け取ったデータをamientノードで送っています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183282/f178257f-f6d6-a4e4-3a99-f7f0e2cc8664.png)


##データ監視
　Ambientでグラフ表示しています。特に難しい設定はしておらず、このようにグラフ表示できました。
今回はデータを見やすくするために、日にち単位で表示させています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183282/efc94bf4-8ea5-44d1-de30-5a2a113d2e47.png)

　1日の様子を計測しましが、**値のブレが大きいい**のと、**1日ではなかなか乾かない**ということがわかりました。1日というのも曇りだったので、晴天の直射日光が当たれば乾くスピードも速くなり、データに現れてくると思います。


#おわりに
　実際に使ってみると、土壌湿度データが上限値に張り付いており、正しく計測できているか不安な状態です。ESP32のADCは癖があるようなので、外付けのADCで評価してみようと思いました。今回は補正も行っていないので、その辺も踏まえて取り組んでいった方がよいと分かりました。

　それでも、乾いているか否かぐらいは判断できそうです。

※土壌水分量の計算ってもしかすると次の式であっている？

```
 水分量[%] = デジタル値 / 4095 * 100
```

#ソースコード

```c++

#include <M5Stack.h>
#include <PubSubClient.h> // MQTT
#include <WiFi.h>
#include <WiFiClient.h>
#include <Wire.h>

#define RELAY 3

char *ssid = "*****";            // Wi-FiのSSID
char *password = "*****";              // Wi-Fiのパスワード

// MQTT 定義
const char *endpoint = "broker.shiftr.io";
const int port = 1883;
char *deviceID = "M5Stack";               // デバイスID
char *userName = "*******";              // MQTTブローカ認証ユーザ名
char *userPass = "*******";      // MQTTブローカ認証パスワード
char *pubTopic = "/pub/";          // メッセージを知らせるトピック
char *subTopic = "/sub/";          // メッセージを待つトピック
WiFiClient httpsClient;
PubSubClient mqttClient(httpsClient);


int data[2];
int avg_data = 0;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  M5.begin();
  pinMode(RELAY, OUTPUT);
  digitalWrite(RELAY,LOW);
  data[0] = analogRead(35);

  // Wi-Fi設定
  WiFi_init();

  // connect MQTT
  mqttClient.setServer(endpoint, port);
  mqttClient.setCallback(mqttCallback);
  connectMQTT();
    
  M5.Lcd.setTextColor(TFT_WHITE, TFT_BLACK);
  M5.Lcd.setTextSize(3);
  Serial.println("Start!");
}

void mqttLoop() {
    if (!mqttClient.connected()) {
        connectMQTT();
    }
    mqttClient.loop();
}

long messageSentAt = 0;
int count = 0;
char pubMessage[128];
char f_moter= 0;

void loop() {
  data[1] = analogRead(35);   // new data
  avg_data = (data[1] + data[0]) / 2; 
  Serial.print("Moisture Sensor Value:");
  Serial.println(avg_data);

  // 乾いてきたら給水
  if(avg_data <= 300){
    Serial.println("Moter ON");
    digitalWrite(RELAY,HIGH);
    delay(4000);
    digitalWrite(RELAY,LOW);
    Serial.println("Moter OFF");
    f_moter = 1;
  }else{
    f_moter = 0;
  }
  data[0] = data[1];

  // 5分間隔でデータアップロード
  // MQTT 送信
  sprintf(pubMessage, "{\"d1\": %d,\"d2\": %d}", avg_data, f_moter);
  Serial.print("Publishing message to topic ");
  Serial.println(pubTopic);
  Serial.println(pubMessage);
  mqttClient.publish(pubTopic, pubMessage);
  
  mqttLoop();
  delay(10000);
}

void WiFi_init(void){
  // WiFi setting
  Serial.println("Connecting to ");
  Serial.print(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      Serial.print(".");
  }

  // WiFi Connected
  Serial.println("\nWiFi Connected.");
  M5.Lcd.setCursor(10, 40);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(3);
  M5.Lcd.printf("WiFi Connected.");
}

void connectMQTT() {
  mqttClient.setServer(endpoint, port);
  mqttClient.setCallback(mqttCallback);
  while (!mqttClient.connected()) {
      if (mqttClient.connect(deviceID,userName,userPass)) {
          Serial.println("MQTT Connected.");
          int qos = 0;
          mqttClient.subscribe(subTopic, qos);
          Serial.println("Subscribed.");
      } else {
          Serial.print("Failed. Error state=");
          Serial.print(mqttClient.state());
          // Wait 5 seconds before retrying
          delay(5000);
      }
  }
}

void mqttCallback (char* topic, byte* payload, unsigned int length) {
    String str = "";
    static int pump[2] = {0,0};
    Serial.print("Received. topic=");
    Serial.println(topic);
    
}
``