【SALZ製作】循環式自動潅水装置SALZ3完成しました。
盆栽向けの循環式自動潅水装置、SALZ3がようやく完成しました。
SALZ3の給水テスト中水分計を外し、常に乾燥状態と判断させて、動作チェックを行っているところ
現在、稼働テスト中ですが、お披露目できるレベルに達したと判断し、公開いたします。
テスト用の鉢は今うちの棚場で一番乾きが早いピラカンサス。
できること:
・静電容量方式水分計で土壌の水分量をモニタリングします。
・モニタリングした数値は、Wifiを通じて、クラウドのAmbientサービスへ送られ、蓄積、視覚化されます。PCでもスマホでも、どこからでも鉢の乾き具合、SALZ3の稼働状態をグラフで観察することができます。
・水分量が一定の閾値を超えると、サーボモーターを動かして、排水弁を閉じ、リレーをオンにし、水中モーターを一定時間動作させます。
・一定時間、ドブ浸け状態を維持します。
・排水弁を開き、排水します。
・コントローラの画面を押すと、LEDが表示し、閾値の設定ができます。左右に傾けると赤い矢印が出てきますので、増減させたい矢印が表示された状態でボタンを押すと、閾値が変更できます。
必要なもの:
・Wifi
・データの送信に必要です。
・電源
・100V電源またはモバイルブースター
100V電源は、漏電保護タップを用いて、安全に配慮しました。
モバイルブースターは短時間での運用であれば稼働します。
こちらも、破裂などの可能性があるため、気を付けます。
・水
・今回はデモンストレーション用装置のため、下部トレーについては穴をあけていませんが、棚場で使用するためには、下部トレーにもオーバーフロー穴をあけておきます。SALZ1/2では、塩ビ管コネクタを取り付けましたが、単に穴をあける形でよいと思います。循環式の潅水装置のため、水の腐敗が気になります。水腐れ防止用にミリオンを少し撒いておくとよいと思います。
コントロール部のアップ。紆余曲折の末、コンパクトに納まりました。
材料(おおよその価格):
・[P01]アステージ製NFボックス#7 400円 または NCボックス#7(蓋つき) 550円
・[P02]アステージ製NFボックス#11 500円 または NCボックス#11(蓋つき) 650円
・[P03]TSバルブソケット25 70円
・[P04]TSスイセンソケット25 80円
・[P05]盆栽用肥料ケース 200円(袋入り)
・[P06]水やりキット(Amazon) 1180円
・[P07]M5ATOM Matrix 1400円
・[P08]ATOMIC DIY PROTO KIT 500円
・[P09]サーボモーターSG90 500円
・[P10]EHコネクタ ベース付きポスト 3P(トップ型) × 3、2P(サイド型) × 2 100円
・[P11]EHコネクタ ソケットハウジング 3P × 3、2P × 1 100円
・[P12]EHコネクタ コンタクト × 11 200円
・[P13]ワイヤ線(AWG#24) 300円
・[P14]USB Type-C ケーブル(プログラム転送、電力供給用) 100円
・[P15]ペットボトルのフタ
・[P16]ペットボトルの上部
・[P17]バスコーク
・[P18]結束バンド
・[P19]ビニール袋
・[P20]ビニールテープ
・[P21]ビー玉
・[P22]テグス糸
・[P23]針金
・[P24]スズめっき線
作り方:
・筐体
[P01]NFボックス#7に給水・オーバーフロー用の穴と排水弁用の穴を開けます。
また、サーボモーター取り付け用の穴も開けます。
・給水ポンプ
[P06]水やりキットの水中モーターの線を伸ばします。このとき、熱収縮チューブなどを使い、防水加工を施します。キットに付属のチューブをはめ込みます。
・排水弁
[P16]ペットボトルの飲み口を切り取り、排水口にしました。[P15]キャップに穴あけをし、[P23]針金をボンドでつけた[P21]ビー玉を栓にしました。水漏れが気になり、[P17]バスコークで、栓とビー玉の漏れを少なくしました。栓の作りに苦戦しています。漏れが少なく、排水性の高い排水栓を探しています。これは今後の課題です。[P22]テグス糸を用いて、[P09]サーボモーターSG90と結びます。サーボモータは角度が決まっていますので、通電し、サーボの動きを確認した後、サーボホーン(サーボの先につける棒)を取り付けます。その後、栓の位置をよく確認し、テグスの長さを調整します。
・オーバーフロー
・オーバーフロー用の穴にTSバルブソケット25、TSスイセンソケット25を取りつけます。TSバルブソケット25を上に向けて取り付けます。この高さでドブ浸け時の水深を決めます。深さが足りないときは、塩ビ管VP25を差し込み、適当な長さで切断します。NFボックス#7の高さを超えないように気を付けます。パイプの中に、給水ホースと、給水ポンプ用の電源を通します。
・マイコン配線
ATOMIC DIY PROTKITを用いて、コネクタ配線できるようにしました。SALZ2ではブレッドボードを用いて配線を行いましたが、SALZ3では、装置を傾けてスイッチを押す機能があるため、コンパクトで操作しやすい作りにしました。ユニバーサル基板は初めて使いました。はんだ付けに苦戦しながらも、なんとか完成させることができました。テスターを用いて導通テストを念入りに行い、配線ミスをなくし、完成させました。コネクタにはEHコネクタを採用。
こんな感じのパッケージです。
取り出してみました。
EHコネクタを取り付けたところ。EHコネクタ2Pを追加発注しました。
仮組みしたところ。良い感じで配置できました。
マスキングテープで部品を仮止めし、裏返しました。
スマホの画像編集アプリを使い、写真に配線予定を書き込みました。あとで確認すると、少し配線が違っていましたが、初めて行う配線作業がとてもはかどりました。
はんだ付けのコツは「いかに手をあけるか」だと思いました。両手ははんだごてと、はんだ線だけを持ち、ほかのものは、固定具を用いることが大切であることを実感しました。ルーペは作業中は慣れなくて使えませんでしたが、確認作業がはかどりました。
今回、ツールの大切さを実感する工作となりました。
汚いはんだ面ですが、自分ではとても満足しています。この時は、少し大きめのワイヤーストリッパーを使っており、ワイヤ線の切断があまりスムーズにできていませんでした。この後、電子工作用のワイヤーストリッパーを手に入れ、とても簡単に被覆を剥けることがわかりました。道具って大切ですね。
苦手な、はんだ付けの克服は、固定具の適切な使用と、導通テスターを用いたチェック方法、間違った時の、はんだ吸い取り線による修正方法を会得したことが大きいです。
今の自分には、これが精いっぱい。後で振り返ると、GNDは極力[P24]スズめっき線で渡したほうが良かったこと。部品面にも配線することで、こんなにワイヤー線てんこ盛りにならなかったのではないでしょうか。
各ワイヤ線の先をコンタクトピンに変更し、ソケットハウジングに差し込みます。圧着工具を使用しての作業になりますが、丁寧にやれば、きちんとできます。何度も書いてしまいますが、やはりきちんとした工具を使って工作すれば、きちんとできるんだということを改めて実感しました。コネクタで接続したために、安心して、接続できます。
はんだ面のワイヤ線がてんこ盛りで筐体に収まるか不安でしたが、うまく収まりました。
ブレッドボードとの比較画像です。確かにコンパクトになりました。ワイヤ線は少しもじゃっとします。
同時に3つ作りましたが、線の長さがまちまちのため、一品一品違ったもののように見えます。導通テスターで動作チェックしているため、問題ありません。
配線図は以下のようになっています。
■3V3-->給水モータ用リレースイッチ VCC
□G21 未使用
□G22 未使用
□G25 未使用
■G19-->排水弁用サーボモータ PWM
■ 5V-->排水弁用サーボモータ VCC、水分計センサ VCC、給水モータ VCC
■G23-->給水モータ用リレースイッチ SIG
■GND-->各GND
■G33-->水分計センサ IN
・Ambientサービス
Wifiを経由してクラウドサービスのAmbientへデータを送信し、グラフ化しています。
ユーザー登録には、メールアドレスとパスワードが必要となります。
ログイン後、「チャネルを作る」を行うと、チャネルID、ライトキーがもらえます。この値を指示して、データ送信を行います。
Ambient ambient;
unsigned int channelId = 0000; // AmbientのチャネルID
const char * writeKey = "XXXX"; // ライトキー
void
setup(void) {
M5.begin(true, false, true);
if (M5.IMU.Init() != 0)
IMU6886Flag = false;
else
IMU6886Flag = true;
WiFi.begin(ssid, password); // Wi-Fi APに接続 ----A
while (WiFi.status() != WL_CONNECTED) { // Wi-Fi AP接続待ち
delay(500);
Serial.print(".");
}
Serial.print("WiFi connected\r\nIP address: ");
Serial.println(WiFi.localIP());
while (!ambient.begin(channelId, writeKey, &client)) { // チャネルIDとライトキーを指定してAmbientの初期化
delay(500);
Serial.print("*");
}
:
}
void
SendData(void) {
static int c = 0;
Serial.printf("SendData():\n");
Serial.printf("1:waterLevel:%d\n", waterLevel);
Serial.printf("2:wlThreshold:%d\n", wlThreshold);
Serial.printf("3:state:%d\n", state);
Serial.printf("4:c:%d\n", c);
ambient.set(1, waterLevel);
ambient.set(2, wlThreshold);
ambient.set(3, state);
ambient.set(4, c++);
ambient.send();
}
で、Ambientへデータを送信しています。
SALZ3初号機のデータを公開チャネルにしました。稼働状況をご覧ください。
・プログラム
SALZ3用に準備したプログラムを用います。
現時点では、WIFIとAmbientサービスのID、PWをハードコーディングしています。各環境における値をセットします。
// SALZ3 bbd
#include <M5Atom.h>
#include <ESP32Servo.h>
#include <Wire.h>
#include "Ambient.h"
//デモモード(ON:1, OFF:0)
//#define DEMO_MODE 1
#define DEMO_MODE 0
#define P(col, row) (((row) * 5) + (col))
#define RGB(r, g, b) (((g) << 16) + ((r) << 8) + (b))
#define SECONDS(s) ((s) * 1000)
#define MINUTES(m) SECONDS(m * 60)
const int INITIAL_WL_THRESHOLD = 2200;
const int WDRN_TIME = (DEMO_MODE) ? MINUTES(2) : MINUTES(10);
const int DOBU_TIME = (DEMO_MODE) ? MINUTES(2) : MINUTES(10);
const int WAIT_TIME = (DEMO_MODE) ? MINUTES(2) : MINUTES(60);
WiFiClient client;
Ambient ambient;
const char * ssid = "XXXX";
const char * password = "XXXX";
unsigned int channelId = 0000; // AmbientのチャネルID
const char * writeKey = "XXXX"; // ライトキー
//ピン配置
// ■3V3
//G21□ □G22
//G25□ ■G19:排水弁用サーボモータ
// 5V■ ■G23:給水モータ用リレースイッチ
//GND■ ■G33:水分計センサ
const uint8_t WD_PIN = 19; // 排水弁用サーボモータ
const uint8_t WS_PIN = 23; // 給水モータ用リレースイッチ
const uint8_t WL_PIN = 33; // 水分計センサ
const uint8_t WD_CH0 = 0; //チャンネル
const float PWM_HLZ = 50.0; //PWM周波数
const uint8_t PWM_LVL = 16; //PWM 16bit(0~65535)
Servo servo1;
// Published values for SG90 servos; adjust if needed
const int minUs = 500;
const int maxUs = 2400;
float accX = 0, accY = 0, accZ = 0;
float gyroX = 0, gyroY = 0, gyroZ = 0;
float temp = 0;
bool IMU6886Flag = false;
int waterLevel = 0;
int wlThreshold = INITIAL_WL_THRESHOLD;
enum {
ST_WDRN,
ST_DOBU,
ST_WAIT
};
int state = ST_WDRN;
void
CheckIMU6886(void) {
if (IMU6886Flag == true) {
M5.IMU.getGyroData(&gyroX, &gyroY, &gyroZ);
M5.IMU.getAccelData(&accX, &accY, &accZ);
M5.IMU.getTempData(&temp);
// Serial.printf("%6.2f,%6.2f,%6.2f o/s \r\n", gyroX, gyroY, gyroZ);
// Serial.printf("%6.2f,%6.2f,%6.2f mg\r\n", accX * 1000, accY * 1000, accZ * 1000);
// Serial.printf("Temperature : %6.2f C \r\n", temp);
}
}
void
DispLevel(int lv) {
int it = lv / 1000;
int ih = (lv % 1000) / 100;
static int lvPrev = 0;
if (lv == lvPrev)
return;
lvPrev = lv;
Serial.printf("level:%d \r\n", lv);
for (int i = 0 ; i < 5; i++) {
M5.dis.drawpix(P(i, 0), RGB(0, 0xf0, 0) * (it > i));
}
M5.dis.drawpix(P(0, 1), RGB(0, 0xf0, 0xf0) * (ih > 0));
M5.dis.drawpix(P(0, 2), RGB(0, 0xf0, 0xf0) * (ih > 1));
M5.dis.drawpix(P(1, 1), RGB(0, 0xf0, 0xf0) * (ih > 2));
M5.dis.drawpix(P(1, 2), RGB(0, 0xf0, 0xf0) * (ih > 3));
M5.dis.drawpix(P(2, 1), RGB(0, 0xf0, 0xf0) * (ih > 4));
M5.dis.drawpix(P(2, 2), RGB(0, 0xf0, 0xf0) * (ih > 5));
M5.dis.drawpix(P(3, 1), RGB(0, 0xf0, 0xf0) * (ih > 6));
M5.dis.drawpix(P(3, 2), RGB(0, 0xf0, 0xf0) * (ih > 7));
M5.dis.drawpix(P(4, 1), RGB(0, 0xf0, 0xf0) * (ih > 8));
M5.dis.drawpix(P(4, 2), RGB(0, 0xf0, 0xf0) * (ih > 9));
}
void
SettingLoop(void) {
static bool isSetting = false;
static unsigned long prevMillis = 0L;
unsigned long curMillis = millis();
if (isSetting && (curMillis - prevMillis > SECONDS(10))) {
isSetting = false;
prevMillis = curMillis;
Serial.println("SETTING OFF");
M5.dis.clear();
goto EXIT_SettingLoop;
}
CheckIMU6886();
if (M5.Btn.wasPressed()) {
//Serial.printf("isSetting:%d\n", isSetting);
//Serial.printf("curMillis:%ld\n", curMillis);
//Serial.printf("prevMillis:%ld\n", prevMillis);
prevMillis = curMillis;
if (!isSetting) {
isSetting = true;
Serial.println("SETTING ON");
}
if (accX > 0.2)
wlThreshold += 100;
else if (accX < -0.2)
wlThreshold -= 100;
if (wlThreshold < 0)
wlThreshold = 0;
if (wlThreshold > 5000)
wlThreshold = 5000;
DispLevel(wlThreshold);
}
if (isSetting) {
if (accX > 0.2) {
M5.dis.drawpix(P(0, 4), 0);
M5.dis.drawpix(P(1, 3), 0);
M5.dis.drawpix(P(1, 4), 0);
M5.dis.drawpix(P(4, 4), RGB(0xf0, 0, 0));
M5.dis.drawpix(P(3, 3), RGB(0xf0, 0, 0));
M5.dis.drawpix(P(3, 4), RGB(0xf0, 0, 0));
}
else if (accX < -0.2) {
M5.dis.drawpix(P(0, 4), RGB(0xf0, 0, 0));
M5.dis.drawpix(P(1, 3), RGB(0xf0, 0, 0));
M5.dis.drawpix(P(1, 4), RGB(0xf0, 0, 0));
M5.dis.drawpix(P(4, 4), 0);
M5.dis.drawpix(P(3, 3), 0);
M5.dis.drawpix(P(3, 4), 0);
}
}
else {
M5.dis.drawpix(P(0, 4), 0);
M5.dis.drawpix(P(1, 3), 0);
M5.dis.drawpix(P(1, 4), 0);
M5.dis.drawpix(P(4, 4), 0);
M5.dis.drawpix(P(3, 3), 0);
M5.dis.drawpix(P(3, 4), 0);
}
EXIT_SettingLoop:
M5.update();
}
void
WdOpen(void) {
//排水弁を開く
Serial.println("排水弁を開く");
servo1.write(170);
delay(1000);
}
void
WdClose(void) {
//排水弁を閉じる
Serial.println("排水弁を閉じる");
servo1.write(10);
delay(1000);
}
void
WsOn(void) {
//給水On
Serial.println("給水On");
digitalWrite(WS_PIN, LOW);
}
void
WsOff(void) {
//給水Off
Serial.println("給水Off");
digitalWrite(WS_PIN, HIGH);
}
void
WsInit(void) {
pinMode(WS_PIN, OUTPUT);
}
void
WdInit(void) {
servo1.setPeriodHertz(PWM_HLZ);
servo1.attach(WD_PIN, minUs, maxUs);
}
void
WlInit(void) {
pinMode(WL_PIN, INPUT_PULLUP);
}
void
WdTest(void) {
// 排水弁のテスト
for (int i = 0; i < 1; i++) {
WdOpen();
WdClose();
}
}
void
WsTest(void) {
// 給水テスト
Serial.println("WsTest() Start");
for (int i = 0; i < 1; i++) {
WsOn();
delay(3000);
WsOff();
delay(3000);
}
Serial.println("WsTest() End");
}
float
GetWl(void)
{
float val = 0.0;
pinMode(WL_PIN, INPUT_PULLUP);
for (int i = 0; i < 5; i++) {
float v = analogRead(WL_PIN);
//Serial.printf("ML(%d):%8.3f\r\n", i, v);
val += v;
}
return (val / 5);
}
void
WlTest(void) {
for (int i = 0; i < 5; i++) {
GetWl();
}
}
void
InitialTest(void) {
Serial.println("InitialTest()");
WsTest();
WdTest();
WlTest();
}
void
SendData(void) {
static int c = 0;
Serial.printf("SendData():\n");
Serial.printf("1:waterLevel:%d\n", waterLevel);
Serial.printf("2:wlThreshold:%d\n", wlThreshold);
Serial.printf("3:state:%d\n", state);
Serial.printf("4:c:%d\n", c);
ambient.set(1, waterLevel);
ambient.set(2, wlThreshold);
ambient.set(3, state);
ambient.set(4, c++);
ambient.send();
}
void
Task0(void * arg) {
while (1) {
SettingLoop();
}
}
//---------------------------------------
void
setup(void) {
M5.begin(true, false, true);
if (M5.IMU.Init() != 0)
IMU6886Flag = false;
else
IMU6886Flag = true;
WiFi.begin(ssid, password); // Wi-Fi APに接続 ----A
while (WiFi.status() != WL_CONNECTED) { // Wi-Fi AP接続待ち
delay(500);
Serial.print(".");
}
Serial.print("WiFi connected\r\nIP address: ");
Serial.println(WiFi.localIP());
while (!ambient.begin(channelId, writeKey, &client)) { // チャネルIDとライトキーを指定してAmbientの初期化
delay(500);
Serial.print("*");
}
WsInit();
WdInit();
WlInit();
InitialTest();
//初期状態のセット
WsOff();
WdOpen();
waterLevel = GetWl();
state = ST_WAIT;
SendData();
xTaskCreatePinnedToCore(Task0, "Task0", 4096, NULL, 1, NULL, 0);
}
void
loop(void) {
waterLevel = GetWl();
Serial.printf("waterLevel:%d\n", waterLevel);
if(waterLevel > wlThreshold) {
Serial.println("ドライ");
state = ST_WDRN;
SendData();
WdClose();
WsOn();
Serial.printf("給水:%d秒\n", WDRN_TIME / 1000);
delay(WDRN_TIME);
state = ST_DOBU;
SendData();
WsOff();
Serial.printf("ドブ浸け:%d秒\n", DOBU_TIME / 1000);
delay(DOBU_TIME);
WdOpen();
}
state = ST_WAIT;
SendData();
Serial.printf("待機:%d秒\n", WAIT_TIME / 1000);
delay(WAIT_TIME);
}
次への課題:
・Wifiがないときの処理を丁寧に作る。
・現時点では、指定されたWifiが必ずあるものとして、接続できるまで待ち続けます。Wifiはデータ送信のために必要な機能ですが、水やり装置としては、先に進めると思います。今後の改良点にいたします。
・いま!ボタンの追加
・SALZ2での改良点として、閾値の設定がありました。今回、閾値を設定することが可能となりましたが、あともうちょっとUIに工夫をしたいと考えています。これが、「いま!ボタン」です。このボタンを押すと、閾値を現在の値か過去の「いま!ボタン」の値の平均値にセットし、水やりを行います。その鉢にあったベストな水やりタイミングを得られます。
・もっと簡単に作れるように工夫する。
・SALZ3は電子工作、水回り工作、プログラミングなど、様々な要素が絡み合っています。
・要素ごとに、簡単にできる方法はないか、模索する必要がありそうです。
・SALZ3初号機と同じ作りで、ひと回り大きくした NFボックス#13、NFボックス#25を用いて作成したいと考えています。
・お盆休みにここまでやろうと思っていたのですが、残作業になってしまいました。
#bonsai #盆栽 #循環式自動潅水装置 #SALZ #電子工作 #M5Atom #園芸 #WaterWorks #M5Stack #ArduinoIDE #SG90 #水分計 #リレー