前回までで、フロート式液面計を導入し、ちゃんと機能することがわかりました。しかし、キュウリの蔓が2mを超えるくらい成長すると、やはり水槽内の根がすごい量になるので、駆動部分があるフロート式が心配になってきます。水替え機能を検討するにあたって、底部液面計が別途必要になったため、駆動部分がない光学式液面計を試してみることにしました。
FS-IR02/光学式液面計

試してみるのは、DFRobotのFS-IR02です。メーカーサイトの説明によると、特長として駆動部がないこと、校正不要、耐腐食性プローブが挙げられています。まさに水耕栽培の水槽液面管理の為にあるような液面計ですね。
Gravity: Photoelectric High Accuracy Liquid Level Sensor for Arduino / DFRobot
https://www.dfrobot.com/product-1470.html
インターフェースが付いていて、プローブの4線は3線にまとめられていたり、たぶんプルアップとかが付いていて、ESP32C3には電源線(3.3VとGND)と信号線の接続だけで済みます。信号も0/1で出力されるので、処理もラクチンですね。
スケッチはメーカーのサンプルコードを参考にしました。
SEN0205 Liquid Level Sensor FS-IR02 Tutorial / DFRobot
https://wiki.dfrobot.com/Liquid_Level_Sensor-FS-IR02_SKU__SEN0205#target_3
ただ、インクルードするライブラリとかはなく、直接digitalReadをするだけで、アナログ機器並みのシンプルさです。液面計のところだけ切り出すと、スケッチは下のような感じです。
int LI2 = 0; // LOWER_WaterLevel
const int LI2_PIN = D9;
void setup() {
Serial.begin(115200);
pinMode(LI2_PIN, INPUT); //D9
}
void loop() {
LI2 = digitalRead(LI2_PIN);
Serial.println(LI2);
delay(1000);
}
液面を検知して0/1を出力するだけの段階では全く悩みませんでしたが、これを水替え機能に組み込むときに軽くハマりました。いや、液面計の部分ではなくて、水替え機能の実装のところですね。
水替え機能/RTC割り込み、排水開始
夏場のキュウリの水耕栽培で、ある程度株が大きくなってくると、結構すぐに水の状態が悪くなってきます。触るとヌルっとした感じがして、キュウリのにおいとは少し違うにおいがうっすらします。5Vエアポンプのエアレーションでは限界があるようなので、毎週末コップで水を汲みだして新しい水を入れる水替えを手作業でやっていました。これを自動化して組み込みます。
実装したい機能は以下の通り。
・RTCアラーム割込みで、水を排出(週一回くらいを想定/排出用水中ポンプ設置)
・水の排出は、底部液面計検知で停止
・底部液面計作動不良に備えて、設定排出時間でも排出停止(冗長化)
・loop()に復帰し、貯水槽から新しい水を補給
・水の補給は上部液面計検知で停止
・外部スイッチでも水替えを割込み実行できる(根が腐り始める等の異常発見時用)
本当は肥料の追加調整も入れたかったですが、一気に全部入れようとすると終わらないので、取り合えず水替えだけにすることにしました。
一つ目のRTCアラーム割込みは、メーカーのチュートリアルにあるサンプルコードほぼそのままです。
Sample Code 2 – Interrupt / SD3031 / DFRobot
https://wiki.dfrobot.com/SKU_DFR0998_Fermion_SD3031_RTC_Module_Breakout#target_7
余分な所を削除し、割り込み関数名や使用するピンを変えています。割り込みのところだけを抜き出すと下のような感じ。ここは以前RTCの回で書いた通りですね。
#include <DFRobot_SD3031.h>
#include <Wire.h>
DFRobot_SD3031 rtc;
const int P1 = D8; //water charge pump
const int P2 = D2; //water discharge pump
int LI1 = 0; // UPPER_WaterLevel
int LI1_PIN = A0;
int LI2 = 0; // LOWER_WaterLevel
const int LI2_PIN = D9;
volatile int8_t alarmFlag = 0; //RTC alarm
unsigned long RTC_alarmTime; // water discharge pump start time
void setup() {
Serial.begin(115200);
while (rtc.begin() != 0) {
Serial.println("Failed to init chip, please check if the chip connection is fine. ");
delay(1000);
}
pinMode(P1, OUTPUT); //D8
pinMode(P2, OUTPUT); //D2
pinMode(Air, OUTPUT); //D10
pinMode(TDS_PIN, INPUT); //A1
pinMode(LI1_PIN, INPUT); //A0
pinMode(LI2_PIN, INPUT); //D9
//D4 --SDA for RTC
//D5 --SCL for RTC
//D6 --vacant
//D7 --INT from RTC
rtc.setAlarm(rtc.eSaturday, 10, 0, 0); //Set weekly timed alarm
attachInterrupt(digitalPinToInterrupt(D7), interrupt, FALLING);
}
void loop() {
sTimeData_t sTime; //now time
sTime = rtc.getRTCTime();
if (alarmFlag == 1) {
LI2 = digitalRead(LI2_PIN);
unsigned long P2_time = millis();
digitalWrite(P2, HIGH);
digitalWrite(P1, LOW);
Serial.println("Water Discharge has been started.");
unsigned long P2_work_time = P2_time - RTC_alarmTime;
Serial.println(RTC_alarmTime, DEC);
Serial.println(P2_time, DEC);
Serial.println(P2_work_time, DEC);
Serial.println(LI2);
delay(1000);
}
void interrupt(void) {
alarmFlag = 1;
RTC_alarmTime = millis();
}
volatile int8_t alarmFlag = 0;
別の関数間をまたぐので、アラームフラグをvolatile型で設定し初期化
rtc.setAlarm(rtc.eSaturday, 10, 0, 0);
アラーム時刻設定、この場合は毎週土曜日10:00:00にアラーム発報。FallingとかRisingとか設定するところがないけど、Fallingしかないのかな。
attachInterrupt(digitalPinToInterrupt(D7), interrupt, FALLING);
XIAO ESP32C3のD7ピンがRTCアラームでの割り込み信号(Falling)を受けた時に、関数interruptを読み出す。
void interrupt(void) {alarmFlag = 1; RTC_alarmTime = millis();
関数interrupの中身。アラームフラグを1に書き換える。RTC_alarmTimeに現在時刻(millis)を記録。
if (alarmFlag == 1) {
アラームフラグが1になった場合、” { “以下を実行。
中身は、底部液面計数値読み出し、現在時刻(millis)記録(P2稼働時間を算出するため)、排水ポンプP2スタート、給水ポンプP1停止(確率は低いけど、給水ポンプP1起動中に割込み、水替えが始まると、給水と排水が続くことになる。給排水ポンプ同時起動防止)、”排水が開始されました”をシリアル表示。排水ポンプP2稼働時間を計算。
上のスケッチでは、if文を抜ける部分を省略しているので、そのまま実行すると永遠に排水が続きます。
ここでハマったのは、主に” = “と” == “の使い分けですね。alarmFlagは1になるのに、if関数が始まらないので、一週間くらいガチャガチャやっていました。結局if関数の条件式を==にすることで解決しましたが、結局何がどう正しいのかいまいちわかりませんでした。なかなか奥が深いですね。動くようになる組み合わせを見つけた感じなので、どこかで勉強しないといけないです。
水替え機能/底部液面検知、排水ポンプ停止
底部液面を検知する為、液面計を最低液面計に設置します。槽壁底部側や槽底に穴を開けて、液面計検出部を水槽内液させることも考えましたが、槽本体は発泡スチロールなので、水漏れ懸念が無いほどの加工精度で穴が開けられないことと、強度を失いそうだったので、穴を開けるのはやめておきました。
結局、上部液面計と同じ様に、塩ビパイプとエンドキャップを使って挿入管を作りました。高さ調整法も全く同じです。上部液面計と違って、常時水中にいることになるので、ねじ込み部分にシールテープを巻いてはみました。まあそれでも水が入りそうな気がするので、ガスケットを用意しないといけないですね。今回はそのまま設置しました。



底部の液面を検知し、排水ポンプP2を停止する部分のスケッチは、if構文を使って下のような感じにしました。液面計の液面検知だけだと、検出器への汚れ付着でうまく液面検知できなかった場合、長時間ポンプが空運転することになります。また、キュウリの根も長期間水のない状態になります。水槽満液から排水しきるのに要する時間を何度か実測したところ、大体15分以内には終わっていることがわかりました。そこで、冗長化の為、液面検知に加えて、排水ポンプP2が排水を開始してからの経過時間も停止条件に入れることにしました。具体的には、底部液面計LI2液面検知、又は排水ポンプP2稼働時間15分(900秒)以内、です。
/* 省略 */
void loop() {
sTimeData_t sTime; //now time
sTime = rtc.getRTCTime();
if (alarmFlag == 1) {
LI2 = digitalRead(LI2_PIN);
unsigned long P2_time = millis();
digitalWrite(P2, HIGH);
digitalWrite(P1, LOW);
Serial.println("Water Discharge has been started.");
unsigned long P2_work_time = P2_time - RTC_alarmTime;
delay(1000);
if (LI2 == 0 || P2_work_time >= 900000) { //Limit setting for P2
digitalWrite(P2, LOW);
Serial.println("Water Discharge was finished.");
rtc.clearAlarm();
alarmFlag = 0;
Serial.println("alarmFlag was cleared.");
delay(1000);
}
} else {
/* 省略 */
delay(1000);
}
}
void interrupt(void) {
alarmFlag = 1;
RTC_alarmTime = millis();
}
主だったところの注釈は以下の通り。
LI2 = digitalRead(LI2_PIN);
底部液面計LI2数値読み出し(0 / 1)
if (LI2 == 0 || P2_work_time >= 900000) {
底部液面計LI2が液面検知(LI2 == 0)、又は排水ポンプP2稼働時間15分(単位はミリ秒、15分 = 9000000ミリ秒)以上なら、” { “以下を実施。内容は、排水ポンプP2停止、排水終了をシリアル表示、RTCのアラート初期化、アラームフラグ初期化、アラームフラグ初期化完了をシリアル表示。
} else {
if文の条件式に該当しない場合はelse { 以下を実施。通常状態で実施する項目。
水替え機能を実装し、水替えを自動で実施した時のAmbientの表示は、以下のようになります。

真ん中下は上部液面計、下部液面計の表示です。

水替え開始は毎週土曜日の10時00分にセットしてあります。RTCからの割り込みで関数interruptが始まると、LI1の数値読み出しは行わなくなります。ぱっと見は10時開始で液面が徐々に減っているように見えますが、その間測定していなくて、次に測定値が出る排水ポンプ停止後から数値が出るようになります。従ってここで分かるのは、10時00分にRTC割り込みがあって、排水ポンプが動き始めたこと、排水ポンプは10時12分に停止してif文を抜けた、ということです。
底部液面計の指示値はずっと0なので、液面検知が上手くいかなかったのかと思いそうですが、そうではなくて、ambientのデータタイミングが5秒に1回なので、拾えていないだけです。排水は底部液面検知、または排水ポンプ稼働15分以上、が停止の条件です。排水ポンプ稼働が12分なのに停止している、ということは、底部液面計が液面を検知した、ということです。底部液面検知と同時に給水が始まるので、データとしては残らなかったですね。10秒くらい待機を入れた方がいいかも。
真ん中上は、上部液面計、給水ポンプP1、給気ブロアB1の状態表示です。

給水ポンプP1は10:12に稼働を始めて、10:30に停止したことが分かります。3段目は給気ブロアB1ですが、その間ずっと給気を継続しています。
左上は水槽温度です。水槽の水が抜けて、新しい水が入って来て温度が少し変わっています。

左下はTDS計ですね。まだ紹介していませんが、TDS値も水の入れ替えに従って大きく変動しています。

ここまでで、自動で水替えが問題なくできました。この形にして1か月経過しましたが、心配していた液面計検出器表面の腐食や変成はなく、耐久性も大丈夫そうな感触です。
水替え機能/外部スイッチ
最後は外部スイッチでの水替えシーケンス割り込み実行です。D7ピンにRTCのINTが接続されていて、HIGHからLOWになるFALLINGを信号として検知しているので、外部スイッチでこれを再現すれば、RTCからの割り込みと同じ形になります。具体的には、330Ω抵抗でD7ピンを別途プルアップし、タクトスイッチを介してGNDと接続しておけば、タクトスイッチオンでGNDに落ちるため、割り込み信号と同様な信号を外部から与えることが出来ます。

ちょっと絵心というかfritzingゴコロが無いので、非常にわかりにくいですが、伝わるでしょうか(なんとか伝われ!)。
RTC(SD3031)INTピンはXIAO ESP32C3のD7ピンに接続されています。ここにタクトスイッチ、330Ω抵抗を通して3.3Vに接続してプルアップしておきます。タクトスイッチを押してオンにすると、D7ピンにかかっている電圧がGNDに落ちるため、RTCからのFALLINGと同様の信号を、D7ピンへ入力することができます。
動作状況の動画は撮影していませんが、動作確認の結果は良好。一回スタートした水替えはスイッチ的に止める手段はありませんが、底部液面計を引き上げると、液面を検知して止めることが出来ます。その内緊急停止ボタンも作ろうかな。
長くなってきたので、とりあえずここまで。次は温度計とTDS計かな。









コメントを残す