2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

水やりセットを買ってしまったので(悲しみを乗り越えて)ササッとESP32でやってみる

2
Posted at

アマゾンでこんなものを買いました。(ちなみに1180円でした)

マニュアルもなんにもなくてどう使えばいいのかさっぱりです。
試行錯誤して水やりできるようになったのでその記録を残します。

土が乾いていたらポンプを開放し、水やりをし、十分に湿っているならポンプを閉じる。みたいなのを作りました。

ざっくりこんな感じ↓

image.png

TL;DR 最後にできたもの

必要な部品

  • 電容量式土壌水分センサー(セットの中に含まれる)
  • ポンプ(セットの中に含まれる)
  • リレーモジュール(セットの中に含まれる)
  • 100均一でも売ってる一般的なUSB用のモバイルバッテリー
  • iPhone用充電ケーブル
  • USB Type-C用ケーブル
  • ジャンパーワイヤーたくさん
  • ESP32C3(https://akizukidenshi.com/catalog/g/g117454/)
  • ロジックレベルコンバータ(https://akizukidenshi.com/catalog/g/g113837/)

配線図

ソースコード

use esp_idf_hal::peripherals::Peripherals;
use esp_idf_hal::gpio::PinDriver;
use esp_idf_hal::adc::attenuation;
use esp_idf_hal::adc::oneshot::config::AdcChannelConfig;
use esp_idf_hal::adc::oneshot::{AdcChannelDriver, AdcDriver};
use esp_idf_hal::delay::FreeRtos;
use esp_idf_svc::log::EspLogger;
use log::info;

// 設定値
const THRESHOLD: u16 = 1600;         // 水分量の基準値(これ以下で乾燥と判定)
const WATERING_TIME_MS: u32 = 5000;  // 水やりをする時間(ミリ秒:ここでは5秒)

fn main() -> anyhow::Result<()> {
    // パッチの適用とロガーの初期化
    esp_idf_hal::sys::link_patches();
    EspLogger::initialize_default();

    let peripherals = Peripherals::take()?;

    // ポンプ用出力はGPIO6
    let mut pump_pin = PinDriver::output(peripherals.pins.gpio6)?;

    // lowで開き、highで閉じる
    pump_pin.set_high()?;

    // 土壌水分センサー用ピンはGPIO4
    let adc = AdcDriver::new(peripherals.adc1)?;
    let config = AdcChannelConfig {
        attenuation: attenuation::DB_12,
        ..Default::default()
    };
    let mut soil_pin = AdcChannelDriver::new(adc, peripherals.pins.gpio4, &config)?;

    loop {
        let val = soil_pin.read()?;
        info!("現在の土壌水分センサーの値 (ADC): {}", val);

        if val < THRESHOLD {
            info!("水やりを開始します");
            
            // ポンプをONにする
            pump_pin.set_low()?;
            FreeRtos::delay_ms(WATERING_TIME_MS);
            
            // ポンプをOFFにする
            pump_pin.set_low()?;
            info!("水やりを終了しました");

            // 水が土全体に浸透するのを待つため、直後の連続判定を防ぐクールダウンを挟む
            FreeRtos::delay_ms(30000); 
        }

        // 1秒ごとにチェック
        FreeRtos::delay_ms(1000);
    }
}

ステップ1:ポンプを動かす

ステップ1.1:単純に動かす

セットの中に入っていたのは電容量式土壌水分センサーと、ポンプが2つ、リレーモジュールが2つでした。
画像ではそれぞれ1個ずつみたいなイメージだったのでちょっとうれしいです。

何はともあれ電源を作ります。
最も入手の簡単な直流電源と言えばUSBのモバイルバッテリーです。USBは5Vでこのポンプも5Vで動作するようです。
余計な銅線が入っていることがあるのでiPhoneの充電用ケーブルを使いますが、市販のiPhoneの充電用ケーブルを剥くと二本の線が出てきます。
どちらがプラスでどちらがマイナスかはテスターで確かめます。これで言うと、赤いほうがプラスのようです。
試しに直接繋げてみるとちゃんと動きます。
昇圧降圧せずに繋げられるのは助かりますね。

ステップ1.2:リレーモジュールと繋げる

単純にポンプを動かすことができたので次はリレーモジュールで制御できるようにします。

まずはソースを動かすRustの準備をします。

cargo install
$ cargo install cargo-generate
$ cargo install ldproxy
$ cargo install espup
$ cargo install espflash
$ cargo install cargo-espflash
$ espup install
$ . ~/export-esp.sh

最初はESP32-WROOM-32E 16MB ( https://akizukidenshi.com/catalog/g/g115673/ )を使っていました。この場合MCUはesp32です。

cargo generate esp-rs/esp-idf-template cargo
⚠   Favorite `esp-rs/esp-idf-template` not found in config, using it as a git repository: https://github.com/esp-rs/esp-idf-template.git

🤷   Project Name: watering-example
🔧   Destination: /home/user/code/watering-example ...
🔧   project-name: watering-example ...
🔧   Generating template ...
✔ 🤷   Which MCU to target? · esp32
✔ 🤷   Configure advanced template options? · false
🔧   Initializing a fresh Git repository
✨   Done! New project created /home/user/code/watering-example

そしてGPIOをオンにしたりオフにするコードを書きます。cargo runでビルドと書き込みができて便利です。

use esp_idf_hal::delay::FreeRtos;
use esp_idf_hal::gpio::PinDriver;
use esp_idf_hal::peripherals::Peripherals;

fn main() -> anyhow::Result<()> {
    esp_idf_svc::sys::link_patches();
    esp_idf_svc::log::EspLogger::initialize_default();
    let peripherals = Peripherals::take()?;
    
    // GPIO 12を出力モードに設定
    let mut pin12 = PinDriver::output(peripherals.pins.gpio12)?;

    loop {
        // 1秒間 ON (High)
        log::info!("gpio12 on");
        pin12.set_low()?;
        FreeRtos::delay_ms(1000);

        // 4秒間 OFF (Low)
        log::info!("gpio12 off");
        pin12.set_high()?;
        FreeRtos::delay_ms(4000);
    }
}

ソフト部分ができたらハード部分です。配線をしていきます。リレーモジュール、ポンプ、ESP32、電源を繋げていきます。
どれも5Vで動きますので昇圧高圧することはなく、USBからの電源を分配するだけで済みました。

リレーモジュールの使い方は探り探りでしたが、こういう感じです。

が、これではうまく行かなかったので色々試行錯誤しました。

トラブル1:ポンプが回らない

とりあえず配線したはいいものの、ポンプが回りません。すぐ止まってしまいます。

原因:パワー不足

電圧を計ってみると、2Vまで低下していました。

思いの外ポンプが電力を使うことがわかったので、一つのモバイルバッテリーにするのではなく、別のモバイルバッテリーを使って、もう一つ電源を用意することにしました。

これでとりあえずは回るようにはなったのですが…

トラブル2:ポンプが回りっぱなし

今度はポンプが回りっぱなしになりました。コード上は開いたり閉じたりを繰り返すのに期待する挙動と合いません。

原因:ESP32の信号の電圧

この時初めて知ったのですが、ESP32のGPIOは3.3Vで出力されます。
対してリレーモジュールは5Vで待ち受けます。
リレーモジュールをOFFにしようと、ESP32から3.3Vの信号を出力したとしても、モジュール内部から見た電圧の差(電位差)は $5V - 3.3V = 1.7V$ となります。

1.7Vの電位差が残ってしまうことで、「オフにしたつもりでも微弱な電流が流れ続けてしまい、リレーが切れなくなる(常にカチッと言ったまま)」という状態が起きます。
それでポンプが回りっぱなしになってしまうわけです。

対策:ロジックレベルコンバータの導入

ESP32のピンが3.3Vの電気を出力するなら5Vに上げてあげれば済む話です。
というわけでロジックレベルコンバータ(https://akizukidenshi.com/catalog/g/g113837/ )を導入しました。
最初、検索に引っかかったのはこっち( https://www.switch-science.com/products/8744 )だったのですが、I2Cである必要はなかったのでそちらを選びました。

おまけ:もし昇圧モジュールで信号を昇圧しようとしたら

ロジックレベルコンバータを導入する前に、3.3Vを5Vにするのだから昇圧モジュールで昇圧すればいいと考えていました。
しかしそれは浅はかな考えでした。
もし仮に5Vの電源線を直結してしまうと、ESP32の3.3V出力端子に高い5Vの電気が逆流してしまい、ESP32のピンが壊れて(ショートして)しまいます。

ロジックレベルコンバータはそこらへんうまくやっており、5Vの電源を別でもらい、3.3Vの信号が来たらそれを検知して、自分(5V電源)の力で新しく5Vの信号を作り直して送り出すことをしてくれます。
回路自体は絶縁されており、高い電圧の電流が混ざることはありません。

うまくできているな〜と思うところでした。

また、ロジックレベルコンバータではなくても、フォトカプラでも良さそうです。
本格的に基盤にするときにはこちらの方を検討します。

ただ、ここまでやってもそれでも期待する動作にはなりませんでしたが…

トラブル3:ESP32に電源が入った時ポンプがちらつく

この回路ではESP32に電源が入った時ポンプがちらついて、不安定になることがありました。
プログラムの問題かと思いましたが、原因は違うところにありました。

原因:誤ったピンの選択

どういうわけかGPIO12を使っていました。このGPIO12は特有の特殊な挙動があります。
GPIO12は Strappingピン という特殊なピンです。起動時の電圧によって、フラッシュメモリの内部電圧(VDD_SDIO)を切り替える役割を持っています。
このピンの内部プルアップや電圧に干渉し、プログラムからの制御が意図通りに効かなくなっていました。

ESP32-WROOM-Eの場合、こういったピンの選び方をします。

  • 最も安全に使える、特別な制限がなく、汎用の入出力(デジタル入出力、PWM、I2C、SPIなどの周辺回路)として自由に使いやすいピン

    • GPIO 4, 16, 17, 18, 19, 21, 22, 23, 25, 26, 27, 32, 33
  • 起動時のモード(ブートモード)を決める役割(ストラップピン)を持っているため、接続する機器や回路によっては起動不良や書き込みエラーの原因になる注意が必要なピン

    • GPIO 0, GPIO 2, GPIO 4, GPIO 12, GPIO 15
    • 起動時に意図せずPull-up/Pull-downされたり、外部から電圧を加えられたりすると、プログラムが起動しなくなります。
    • 使う場合は、外部回路(スイッチやLEDなど)の接続方法に注意し、起動時に不安定にならない工夫が必要です。
  • 入力専用ピン

    • GPIO 34, 35, 36 (VP), 39 (VN)
    • これらは入力しかできません(digitalWriteなどでHIGH/LOWを出力させようとしても動きません)。また、内蔵プルアップ/プルダウン抵抗もありません。センサーなどの読み取り(アナログ入力含む)専用として使います
  • 絶対に使ってはいけない内蔵フラッシュメモリ用ピン

    • GPIO 6, 7, 8, 9, 10, 11
    • モジュール内のフラッシュメモリとチップが通信するために内部接続されています。ここに何かを繋ぐと、プログラムが起動しなくなるか、最悪の場合チップが破損します。

こういったわけでGPIO26を使うことにしました。

ソースコードもそれに合わせて修正します。

use esp_idf_hal::delay::FreeRtos;
use esp_idf_hal::gpio::PinDriver;
use esp_idf_hal::peripherals::Peripherals;

fn main() -> anyhow::Result<()> {
    esp_idf_svc::sys::link_patches();
    esp_idf_svc::log::EspLogger::initialize_default();
    let peripherals = Peripherals::take()?;
    
    // GPIO 26を出力モードに設定
    let mut pin26 = PinDriver::output(peripherals.pins.gpio26)?;

    loop {
        // 1秒間 ON (High)
        log::info!("gpio26 on");
        pin26.set_low()?;
        FreeRtos::delay_ms(1000);

        // 4秒間 OFF (Low)
        log::info!("gpio26 off");
        pin26.set_high()?;
        FreeRtos::delay_ms(4000);
    }
}

これでちらつかなくなりましたが、挙動が思っていたのと少し違っていて、戸惑いました。
それはリレーモジュールの特性によるものでした。

トラブル4:オンにすると水が止まり、オフにすると水が出る

ソースコードでは1秒間水を流し、4秒間待つ。そしてそれを繰り返すよう意図していました。
ですが実際の挙動は意図とは逆に4秒間ポンプが開き、1秒間待つ。これを繰り返していました。

原因:リレーモジュールの特性

市販のリレーモジュールやモータードライバ、フォトカプラなどの多くは、「ピンが LOW のときに ON(作動)、HIGH のときに OFF(停止)」 という逆の論理で動く設計(アクティブ・ロー)になっています。
プログラムで set_low()(オフにするつもり)を実行すると、実際には機器側が ON になってしまいます。

これはプログラムで対処します。highとlowを逆にするだけで期待する挙動になりました。

use esp_idf_hal::delay::FreeRtos;
use esp_idf_hal::gpio::PinDriver;
use esp_idf_hal::peripherals::Peripherals;

fn main() -> anyhow::Result<()> {
    esp_idf_svc::sys::link_patches();
    esp_idf_svc::log::EspLogger::initialize_default();
    let peripherals = Peripherals::take()?;
    
    // GPIO 26を出力モードに設定
    let mut pin26 = PinDriver::output(peripherals.pins.gpio26)?;

    loop {
        // 1秒間 ON (High)
        log::info!("gpio26 on");
        pin26.set_high()?;
        FreeRtos::delay_ms(1000);

        // 4秒間 OFF (Low)
        log::info!("gpio26 off");
        pin26.set_low()?;
        FreeRtos::delay_ms(4000);
    }
}

これでESP32で基本的なポンプの制御ができるようになりました。

次にセンサー部分を見ていきます。

ステップ2:センサーを理解する

こちらはリレーモジュールがない分ハード部分は簡単です。

こういうふうに繋げます。かなりシンプルになりました。

こういうのを書くとアナログ値が取得できます。

use esp_idf_hal::adc::attenuation;
use esp_idf_hal::adc::oneshot::config::AdcChannelConfig;
use esp_idf_hal::adc::oneshot::{AdcChannelDriver, AdcDriver};
use esp_idf_hal::delay::FreeRtos;
use esp_idf_hal::peripherals::Peripherals;

fn main() -> anyhow::Result<()> {
    esp_idf_svc::sys::link_patches();

    let peripherals = Peripherals::take()?;

    let adc = AdcDriver::new(peripherals.adc1)?;

    let config = AdcChannelConfig {
        attenuation: attenuation::DB_12,
        ..Default::default()
    };

    // GPIO 34 をADCチャンネルとして設定
    let mut soil_pin = AdcChannelDriver::new(adc, peripherals.pins.gpio34, &config)?;

    println!("静電容量式土壌水分センサーの読み取りを開始します...");

    loop {
        let val = soil_pin.read()?;
        
        println!("土壌水分センサーの値 (ADC): {}", val);

        FreeRtos::delay_ms(1000);
    }
}

こちらを乾燥した土に差すところまでログに出してみました。その結果がこちらです。

土壌水分センサーの値 (ADC): 2125
土壌水分センサーの値 (ADC): 2122
土壌水分センサーの値 (ADC): 2119
土壌水分センサーの値 (ADC): 2122
土壌水分センサーの値 (ADC): 2122
土壌水分センサーの値 (ADC): 2122
土壌水分センサーの値 (ADC): 2116
土壌水分センサーの値 (ADC): 2123
土壌水分センサーの値 (ADC): 2119
土壌水分センサーの値 (ADC): 2135
土壌水分センサーの値 (ADC): 2121
土壌水分センサーの値 (ADC): 2124
土壌水分センサーの値 (ADC): 2121
土壌水分センサーの値 (ADC): 2124
土壌水分センサーの値 (ADC): 2122
土壌水分センサーの値 (ADC): 2123
土壌水分センサーの値 (ADC): 2122
土壌水分センサーの値 (ADC): 2120
土壌水分センサーの値 (ADC): 2122
土壌水分センサーの値 (ADC): 2140
土壌水分センサーの値 (ADC): 2114
土壌水分センサーの値 (ADC): 2089
土壌水分センサーの値 (ADC): 2077
土壌水分センサーの値 (ADC): 2050
土壌水分センサーの値 (ADC): 2126
土壌水分センサーの値 (ADC): 1805
土壌水分センサーの値 (ADC): 1761
土壌水分センサーの値 (ADC): 1921
土壌水分センサーの値 (ADC): 1703
土壌水分センサーの値 (ADC): 1758
土壌水分センサーの値 (ADC): 1846
土壌水分センサーの値 (ADC): 1848
土壌水分センサーの値 (ADC): 1847
土壌水分センサーの値 (ADC): 1847
土壌水分センサーの値 (ADC): 1847
土壌水分センサーの値 (ADC): 1846
土壌水分センサーの値 (ADC): 1847
土壌水分センサーの値 (ADC): 1847
土壌水分センサーの値 (ADC): 1848
土壌水分センサーの値 (ADC): 1848
土壌水分センサーの値 (ADC): 1846
土壌水分センサーの値 (ADC): 1846
土壌水分センサーの値 (ADC): 1847
土壌水分センサーの値 (ADC): 1789
土壌水分センサーの値 (ADC): 1847
土壌水分センサーの値 (ADC): 1847
土壌水分センサーの値 (ADC): 1845
土壌水分センサーの値 (ADC): 1834
土壌水分センサーの値 (ADC): 1846
土壌水分センサーの値 (ADC): 1846
土壌水分センサーの値 (ADC): 1847
土壌水分センサーの値 (ADC): 1847
土壌水分センサーの値 (ADC): 1858
土壌水分センサーの値 (ADC): 1860
土壌水分センサーの値 (ADC): 1857
土壌水分センサーの値 (ADC): 1863
土壌水分センサーの値 (ADC): 1861
土壌水分センサーの値 (ADC): 1863
土壌水分センサーの値 (ADC): 1860
土壌水分センサーの値 (ADC): 1860
土壌水分センサーの値 (ADC): 1857
土壌水分センサーの値 (ADC): 1867
土壌水分センサーの値 (ADC): 1865
土壌水分センサーの値 (ADC): 1858
土壌水分センサーの値 (ADC): 1860
土壌水分センサーの値 (ADC): 1860
土壌水分センサーの値 (ADC): 1860
土壌水分センサーの値 (ADC): 1857
土壌水分センサーの値 (ADC): 1854
土壌水分センサーの値 (ADC): 1847
土壌水分センサーの値 (ADC): 1620
土壌水分センサーの値 (ADC): 1048
土壌水分センサーの値 (ADC): 1120
土壌水分センサーの値 (ADC): 1135
土壌水分センサーの値 (ADC): 1145
土壌水分センサーの値 (ADC): 1152
土壌水分センサーの値 (ADC): 1158
土壌水分センサーの値 (ADC): 1162
土壌水分センサーの値 (ADC): 1164
土壌水分センサーの値 (ADC): 1167
土壌水分センサーの値 (ADC): 1172
土壌水分センサーの値 (ADC): 1166
土壌水分センサーの値 (ADC): 1174
土壌水分センサーの値 (ADC): 1175
土壌水分センサーの値 (ADC): 1177
土壌水分センサーの値 (ADC): 1176
土壌水分センサーの値 (ADC): 1178
土壌水分センサーの値 (ADC): 1180
土壌水分センサーの値 (ADC): 1179
土壌水分センサーの値 (ADC): 1179
土壌水分センサーの値 (ADC): 1180
土壌水分センサーの値 (ADC): 1182
土壌水分センサーの値 (ADC): 1178
土壌水分センサーの値 (ADC): 1181
土壌水分センサーの値 (ADC): 1185
土壌水分センサーの値 (ADC): 1187
土壌水分センサーの値 (ADC): 1182
土壌水分センサーの値 (ADC): 1185
土壌水分センサーの値 (ADC): 1187
土壌水分センサーの値 (ADC): 1189
土壌水分センサーの値 (ADC): 1190
土壌水分センサーの値 (ADC): 1189
土壌水分センサーの値 (ADC): 1189
土壌水分センサーの値 (ADC): 1189
土壌水分センサーの値 (ADC): 1191
土壌水分センサーの値 (ADC): 1196
土壌水分センサーの値 (ADC): 1196
土壌水分センサーの値 (ADC): 1194
土壌水分センサーの値 (ADC): 1195
土壌水分センサーの値 (ADC): 1196
土壌水分センサーの値 (ADC): 1195
土壌水分センサーの値 (ADC): 1203
土壌水分センサーの値 (ADC): 1195
土壌水分センサーの値 (ADC): 1197
土壌水分センサーの値 (ADC): 1195
土壌水分センサーの値 (ADC): 1190
土壌水分センサーの値 (ADC): 1194
土壌水分センサーの値 (ADC): 1195
土壌水分センサーの値 (ADC): 1195
土壌水分センサーの値 (ADC): 1195
土壌水分センサーの値 (ADC): 1194
土壌水分センサーの値 (ADC): 1195
土壌水分センサーの値 (ADC): 1194
土壌水分センサーの値 (ADC): 1196
土壌水分センサーの値 (ADC): 1196
土壌水分センサーの値 (ADC): 1194
土壌水分センサーの値 (ADC): 1195
土壌水分センサーの値 (ADC): 1197
土壌水分センサーの値 (ADC): 1197
土壌水分センサーの値 (ADC): 1198
土壌水分センサーの値 (ADC): 1198
土壌水分センサーの値 (ADC): 1197
土壌水分センサーの値 (ADC): 1197
土壌水分センサーの値 (ADC): 1198
土壌水分センサーの値 (ADC): 1197
土壌水分センサーの値 (ADC): 1197
土壌水分センサーの値 (ADC): 1200
土壌水分センサーの値 (ADC): 1200
土壌水分センサーの値 (ADC): 1203
土壌水分センサーの値 (ADC): 1204
土壌水分センサーの値 (ADC): 1203
土壌水分センサーの値 (ADC): 1200
土壌水分センサーの値 (ADC): 1205
土壌水分センサーの値 (ADC): 1206
土壌水分センサーの値 (ADC): 1205
土壌水分センサーの値 (ADC): 1202
土壌水分センサーの値 (ADC): 1206
土壌水分センサーの値 (ADC): 1205
土壌水分センサーの値 (ADC): 1205
土壌水分センサーの値 (ADC): 1204
土壌水分センサーの値 (ADC): 1206
土壌水分センサーの値 (ADC): 1206
土壌水分センサーの値 (ADC): 1204
土壌水分センサーの値 (ADC): 1206
土壌水分センサーの値 (ADC): 1211
土壌水分センサーの値 (ADC): 1206
土壌水分センサーの値 (ADC): 1204
土壌水分センサーの値 (ADC): 1206
土壌水分センサーの値 (ADC): 1207
土壌水分センサーの値 (ADC): 1207
土壌水分センサーの値 (ADC): 1198
土壌水分センサーの値 (ADC): 1209
土壌水分センサーの値 (ADC): 1206
土壌水分センサーの値 (ADC): 1209
土壌水分センサーの値 (ADC): 1204
土壌水分センサーの値 (ADC): 1209
土壌水分センサーの値 (ADC): 1209
土壌水分センサーの値 (ADC): 1212
土壌水分センサーの値 (ADC): 1215
土壌水分センサーの値 (ADC): 1215
土壌水分センサーの値 (ADC): 1215
土壌水分センサーの値 (ADC): 1218
土壌水分センサーの値 (ADC): 1214
土壌水分センサーの値 (ADC): 1215
土壌水分センサーの値 (ADC): 1215
土壌水分センサーの値 (ADC): 1216
土壌水分センサーの値 (ADC): 1215
土壌水分センサーの値 (ADC): 1215
土壌水分センサーの値 (ADC): 1218
土壌水分センサーの値 (ADC): 1212
土壌水分センサーの値 (ADC): 1215
土壌水分センサーの値 (ADC): 1216
土壌水分センサーの値 (ADC): 1215
土壌水分センサーの値 (ADC): 1214

これを以下のコードでプロットしてみると、

import matplotlib.pyplot as plt
import numpy as np

data = [
    2125, 2122, 2119, 2122, 2122, 2122, 2116, 2123, 2119, 2135, 2121, 2124, 2121, 2124, 2122, 2123, 2122, 2120, 2122, 2140,
    2114, 2089, 2077, 2050, 2126, 1805, 1761, 1921, 1703, 1758, 1846, 1848, 1847, 1847, 1847, 1846, 1847, 1847, 1848, 1848,
    1846, 1846, 1847, 1789, 1847, 1847, 1845, 1834, 1846, 1846, 1847, 1847, 1858, 1860, 1857, 1863, 1861, 1863, 1860, 1860,
    1857, 1867, 1865, 1858, 1860, 1860, 1860, 1857, 1854, 1847, 1620, 1048, 1120, 1135, 1145, 1152, 1158, 1162, 1164, 1167,
    1172, 1166, 1174, 1175, 1177, 1176, 1178, 1180, 1179, 1179, 1180, 1182, 1178, 1181, 1185, 1187, 1182, 1185, 1187, 1189,
    1190, 1189, 1189, 1189, 1191, 1196, 1196, 1194, 1195, 1196, 1195, 1203, 1195, 1197, 1195, 1190, 1194, 1195, 1195, 1195,
    1194, 1195, 1194, 1196, 1196, 1194, 1195, 1197, 1197, 1198, 1198, 1197, 1197, 1198, 1197, 1197, 1200, 1200, 1203, 1204,
    1203, 1200, 1205, 1206, 1205, 1202, 1206, 1205, 1205, 1204, 1206, 1206, 1204, 1206, 1211, 1206, 1204, 1206, 1207, 1207,
    1198, 1209, 1206, 1209, 1204, 1209, 1209, 1212, 1215, 1215, 1215, 1218, 1214, 1215, 1215, 1216, 1215, 1215, 1218, 1212,
    1215, 1216, 1215, 1214
]

time = list(range(len(data)))

plt.figure(figsize=(10, 5))
plt.plot(time, data, marker='o', linestyle='-', markersize=3, color='b')
plt.title('1-Second Interval Plot')
plt.xlabel('Time (seconds)')
plt.ylabel('Value')
plt.grid(True)
plt.tight_layout()
plt.savefig('plot.png')
plt.show()

こうなります。

plot.png

25秒付近で土に入れ、70秒付近で水をあげました。それがプロットにも表れています。
簡単にまとめるとこうです。

  • 第1フェーズ(0〜24秒): 高めの値で安定

  • センサーが空気中にある状態。

  • 値は平均約2121(2114〜2140の範囲)で推移しており、この期間は非常に安定しています。

  • 第2フェーズ(25〜70秒): 中程度の値へ移行

  • センサーを乾いた土壌に挿入した状態。

  • 25秒目あたりから値が下がり、平均約1847のラインで安定します。途中、一時的な小さなブレがいくつか見られます。

  • 第3フェーズ(71〜73秒): 急激な数値の低下

  • 水分量が非常に多い状態、あるいは水に直接浸かった(または水滴が大量にかかった)瞬間。

  • 71秒目に1620へ下がり、72秒目には今回の計測で最も低い1048まで急降下しています。

  • 第4フェーズ(74〜184秒): 徐々に上昇して再び安定

  • 水から引き揚げた後、あるいは湿った土壌になじんで落ち着いた状態。

  • 1048を底として、その後はゆっくりと数値を戻しながら上昇し、74秒以降は1200〜1215付近で極めて安定した推移を示しています。

まとめると、このセンサーでは、「土が乾いているときほどADC値が高くなり0で一番湿度が高く、湿っているときほど値が低くなり、4095で一番湿度が低い(乾いている)」という特性を持っています。
てっきり湿度がそのまま来ると思っていたので驚きです。

理由は以下のとおりです。

  • 誘電率の仕組み: 水は空気や乾燥した土壌に比べて、電気を蓄えやすい性質(高い誘電率)を持っています。土が湿るとセンサー周辺の静電容量が増加します。
  • 出力電圧の変化: 内蔵されている回路の仕様上、静電容量が増加する(=水分が増える)と、出力されるアナログ電圧が下がる仕組みになっています。そのため、乾燥状態では電圧が高く(ADC値が大きくなり)、濡れると電圧が低く(ADC値が小さく)なります。

湿度に変換してパーセント表示にしてもよいのですが、今回はそのままでいきます。

1900〜1200のあたりを推移してそうなので、1600にしきい値を定めれば土が乾燥したとみなして水やりできそうです。

配線図

use esp_idf_hal::peripherals::Peripherals;
use esp_idf_hal::gpio::PinDriver;
use esp_idf_hal::adc::attenuation;
use esp_idf_hal::adc::oneshot::config::AdcChannelConfig;
use esp_idf_hal::adc::oneshot::{AdcChannelDriver, AdcDriver};
use esp_idf_hal::delay::FreeRtos;
use esp_idf_svc::log::EspLogger;
use log::info;

// 設定値
const THRESHOLD: u16 = 1600;         // 水分量の基準値(これ以下で乾燥と判定)
const WATERING_TIME_MS: u32 = 5000;  // 水やりをする時間(ミリ秒:ここでは5秒)

fn main() -> anyhow::Result<()> {
    // パッチの適用とロガーの初期化
    esp_idf_hal::sys::link_patches();
    EspLogger::initialize_default();

    let peripherals = Peripherals::take()?;

    // ポンプ用出力はGPIO6
    let mut pump_pin = PinDriver::output(peripherals.pins.gpio26)?;

    // lowで開き、highで閉じる
    pump_pin.set_high()?;

    // 土壌水分センサー用ピンはGPIO4
    let adc = AdcDriver::new(peripherals.adc1)?;
    let config = AdcChannelConfig {
        attenuation: attenuation::DB_12,
        ..Default::default()
    };
    let mut soil_pin = AdcChannelDriver::new(adc, peripherals.pins.gpio34, &config)?;

    loop {
        let val = soil_pin.read()?;
        info!("現在の土壌水分センサーの値 (ADC): {}", val);

        if val < THRESHOLD {
            info!("水やりを開始します");
            
            // ポンプをONにする
            pump_pin.set_low()?;
            FreeRtos::delay_ms(WATERING_TIME_MS);
            
            // ポンプをOFFにする
            pump_pin.set_low()?;
            info!("水やりを終了しました");

            // 水が土全体に浸透するのを待つため、直後の連続判定を防ぐクールダウンを挟む
            FreeRtos::delay_ms(30000); 
        }

        // 1秒ごとにチェック
        FreeRtos::delay_ms(1000);
    }
}

失敗:センサーに挿してなくても値がとれてしまう

以下は間違えて別のピンに挿していたときのログです。
てっきりセンサーに挿していなかったらずっと0だと思っていたのでセンサーが壊れているのだと勘違いしていました。
このような数値の変動が起きる原因は、ADCピンがアナログの信号として「空中からの電磁ノイズをアンテナのように拾ってしまうから」です。

ピンがどこにも繋がっていない状態(オープン状態)では、電位が不安定になり、周囲の微弱な電磁波(商用電源のノイズ、Wi-Fi、静電気、人の身体など)を拾って値がランダムに上下します。ESP32のADCは高感度なため、このようなノイズを拾いやすくなります。

土壌水分センサーの値 (ADC): 0
土壌水分センサーの値 (ADC): 0
土壌水分センサーの値 (ADC): 28
土壌水分センサーの値 (ADC): 181
土壌水分センサーの値 (ADC): 780
土壌水分センサーの値 (ADC): 1128
土壌水分センサーの値 (ADC): 1531
土壌水分センサーの値 (ADC): 1731
土壌水分センサーの値 (ADC): 1706
土壌水分センサーの値 (ADC): 1881
土壌水分センサーの値 (ADC): 2265
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2302
土壌水分センサーの値 (ADC): 2305
土壌水分センサーの値 (ADC): 2202
土壌水分センサーの値 (ADC): 2205
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2412
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2306
土壌水分センサーの値 (ADC): 2288
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2380
土壌水分センサーの値 (ADC): 2347
土壌水分センサーの値 (ADC): 2393
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2344
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2055
土壌水分センサーの値 (ADC): 2358
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2229
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2281
土壌水分センサーの値 (ADC): 2344
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2123
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2256
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2450
土壌水分センサーの値 (ADC): 2201
土壌水分センサーの値 (ADC): 1818
土壌水分センサーの値 (ADC): 1213
土壌水分センサーの値 (ADC): 1177
土壌水分センサーの値 (ADC): 918
土壌水分センサーの値 (ADC): 820
土壌水分センサーの値 (ADC): 906
土壌水分センサーの値 (ADC): 794
土壌水分センサーの値 (ADC): 422
土壌水分センサーの値 (ADC): 557
土壌水分センサーの値 (ADC): 344
土壌水分センサーの値 (ADC): 326
土壌水分センサーの値 (ADC): 277
土壌水分センサーの値 (ADC): 96
土壌水分センサーの値 (ADC): 0
土壌水分センサーの値 (ADC): 0
土壌水分センサーの値 (ADC): 4
土壌水分センサーの値 (ADC): 71
土壌水分センサーの値 (ADC): 24
土壌水分センサーの値 (ADC): 0
土壌水分センサーの値 (ADC): 0
土壌水分センサーの値 (ADC): 0
土壌水分センサーの値 (ADC): 0
土壌水分センサーの値 (ADC): 0
土壌水分センサーの値 (ADC): 0
土壌水分センサーの値 (ADC): 0
土壌水分センサーの値 (ADC): 0
土壌水分センサーの値 (ADC): 135
土壌水分センサーの値 (ADC): 67
土壌水分センサーの値 (ADC): 181
土壌水分センサーの値 (ADC): 0
土壌水分センサーの値 (ADC): 7
土壌水分センサーの値 (ADC): 0
土壌水分センサーの値 (ADC): 0
土壌水分センサーの値 (ADC): 0
土壌水分センサーの値 (ADC): 8

参考:ADC1, ADC2, それ以外の違い

項目 ADC1 (アナログ入力) ADC2 (アナログ入力) アナログ機能なし (デジタル専用など)
主な特徴 いつでも自由に使える Wi-Fi使用時は使えない アナログ読み取りはできない
Wi-Fiとの干渉 なし(独立している) あり(競合する) ー
おすすめ用途 センサーなどのアナログ値読み取り全般 Wi-Fiを使わないシステム、またはWi-Fi停止時のみ GPIO制御、PWM出力、通信(I2C/SPIなど)

ADC1 が割り当てられたピン

  • 特徴: 最も使いやすいアナログ入力です。
  • メリット: Wi-FiやBluetoothの通信機能が動いていても、影響を受けずにいつでも安定してアナログ値を読み取ることができます。
  • 方針: アナログセンサー(温度、照度、距離センサーなど)をつなぐ場合は、まず第一にADC1のピンを選ぶのが鉄則です。

ADC2 が割り当てられたピン

  • 特徴: 一見すると便利そうに見えますが、大きな制約があります。
  • デメリット: ESP32のWi-Fiドライバ(およびBluetooth)が内部でADC2のハードウェアを共用しているため、Wi-Fiが有効な状態(通信中など)では、ADC2の値が正しく取得できなくなったり、最悪の場合プログラムがブロックされたりします。
  • 方針: 「Wi-Fiを一切使わない(BLEのみ、あるいはオフライン)」という場合以外は、アナログ入力としての使用は避けた方が無難です。

何もない(アナログ機能がない)」ピン

  • 特徴: 内部にADC(アナログ・デジタル・コンバーター)が繋がっていないピンです。
  • 用途:
  • 通常のデジタル入出力(HIGH/LOWの判定やLED制御など)
  • PWM出力(モーター制御や調光など)
  • I2C、SPI、UARTなどの通信ピン
  • 電源ピンや特殊な制御ピン(ENやGNDなど)

ステップ3:統合、そして悲劇

配線やソフトを色々試してきましたが、後はこれを組み合わせるだけです。
ただ、実際に水を入れたりする内に事故は起こります。
今思えば水と機械なんて相性の悪いものもっと注意して扱うべきでした。

今回、最も強く胸に刻むべき教訓です。

$\huge{ESP32が水没しました}$

その時の動画がこちらです。

本当はちゃんと水やりできているところを撮りたかったのですが、植木鉢から外れて基板の上へ…

PCに挿してもESP32として認識されません。

彼との冒険は始まったばかりなのに終わってしまいました…

仕方がないので別のESP32を使います。

ESP32C3がまだ別にあったので、ポンプ用のピンを26から6に変え、センサー用のピンを34から4に変えて、この水やりシステムは稼働できました。冒頭で載せた図の再掲になりますが、これで完成形になります。

use esp_idf_hal::peripherals::Peripherals;
use esp_idf_hal::gpio::PinDriver;
use esp_idf_hal::adc::attenuation;
use esp_idf_hal::adc::oneshot::config::AdcChannelConfig;
use esp_idf_hal::adc::oneshot::{AdcChannelDriver, AdcDriver};
use esp_idf_hal::delay::FreeRtos;
use esp_idf_svc::log::EspLogger;
use log::info;

// 設定値
const THRESHOLD: u16 = 1600;         // 水分量の基準値(これ以下で乾燥と判定)
const WATERING_TIME_MS: u32 = 5000;  // 水やりをする時間(ミリ秒:ここでは5秒)

fn main() -> anyhow::Result<()> {
    // パッチの適用とロガーの初期化
    esp_idf_hal::sys::link_patches();
    EspLogger::initialize_default();

    let peripherals = Peripherals::take()?;

    // ポンプ用出力はGPIO6
    let mut pump_pin = PinDriver::output(peripherals.pins.gpio6)?;

    // lowで開き、highで閉じる
    pump_pin.set_high()?;

    // 土壌水分センサー用ピンはGPIO4
    let adc = AdcDriver::new(peripherals.adc1)?;
    let config = AdcChannelConfig {
        attenuation: attenuation::DB_12,
        ..Default::default()
    };
    let mut soil_pin = AdcChannelDriver::new(adc, peripherals.pins.gpio4, &config)?;

    loop {
        let val = soil_pin.read()?;
        info!("現在の土壌水分センサーの値 (ADC): {}", val);

        if val < THRESHOLD {
            info!("水やりを開始します");
            
            // ポンプをONにする
            pump_pin.set_low()?;
            FreeRtos::delay_ms(WATERING_TIME_MS);
            
            // ポンプをOFFにする
            pump_pin.set_low()?;
            info!("水やりを終了しました");

            // 水が土全体に浸透するのを待つため、直後の連続判定を防ぐクールダウンを挟む
            FreeRtos::delay_ms(30000); 
        }

        // 1秒ごとにチェック
        FreeRtos::delay_ms(1000);
    }
}

感想

水やり用のセットは安価でしたがそれに必要な部品は見慣れないものが多く、専門的な知識も必要です。
今回の電子工作では作るのに必要なものを揃えるだけでも大変勉強になりましたし、何より楽しかったです。
圧倒的に周辺の必要な部品やツールの方が高かったのですが、それも含めていい思い出です。

水没させてごめんね…

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?