Arduinoスマート温室プロジェクト:センサー、水やり、ファン、コード
Arduinoでスマート温室を作ります。DHT22、土壌水分センサー、LDRによる計測、リレーとMOSFETによるファン・ヒーター・ポンプ・LEDの制御、ヒステリシス、LCD、ESP32のコードまで解説します。
Arduinoスマート温室プロジェクトは、ArduinoやESP32が温室内の気温、湿度、土壌水分、明るさを測り、ファン、ヒーター、水ポンプ、植物育成ライトを自動で動かす自動化システムです。結果が目に見えてわかりやすいため、科学コンテスト、学校のコンクール、技術の授業で最も人気のあるテーマの1つです。土が乾くとポンプが動き、中が暑くなるとファンが回り、夕方になるとLEDが点灯します。さらに、収納ボックスで作った模型の温室とベランダに置く本物のミニ温室とでは、回路もコードもほとんど変わりません。
このガイドでは、まず植物が実際に何を必要としているかを確認し、模型の温室をステップごとに作ります。部品リストとおおよその費用、センサー(DHT22、静電容量式の土壌水分センサー、LDR)とアクチュエーター(リレーとMOSFETで動かすファン、ヒーター、ポンプ、LED)の配線、電源の計画、ピンの表を順に見ていきます。次に、リレーが絶えずカチカチと切り替わるのを防ぐしきい値とヒステリシスの考え方、delay()を使わない完全なArduinoのコード、16x2のLCDを説明します。最後のセクションでは、ESP32を使ってスマホから温室を監視する方法、Raspberry Pi Pico用のMicroPython版、学校や科学コンテストでの発表のコツ、よくある失敗を紹介します。





スマート温室はどのように動く?
どのスマート温室も、測る、判断する、動かす、表示するという同じ4段階のループで動いています。センサーが環境を測ります。中はどれくらい暖かいか、空気はどれくらい湿っているか、土は乾いたか、光は足りているか、といったことです。Arduinoはこれらの値を、自分で決めたルールと比較します。「温度が30 °Cを超えたらファンを回す」「土壌水分が35%を下回ったら水をやる」といったルールです。アクチュエーターがその判断を実行します。リレーがファン、ヒーター、ポンプをオン・オフし、MOSFETがLEDテープの明るさを調整します。最後に、LCDとスマホに温室の現在の状態が表示されます。実際の商業用温室の環境制御コンピューターもまさにこのループで動いていて、センサーの数が多く、モーターが大きいだけです。
| 何を測る? | センサー | アクチュエーター | ルール(例) |
|---|---|---|---|
| 気温 | DHT22 | ファンとヒーター | 30 °Cでファンが回り、16 °Cでヒーターがオン |
| 相対湿度 | DHT22 | ファン | 85%でファンが回り、75%で止まる |
| 土壌水分 | 静電容量式の土壌水分センサー | 小型の水中ポンプ | 35%を下回ると3秒ずつ区切って水やり |
| 光 | LDR | LEDテープ(MOSFET) | 暗くなるとLEDが点灯し、最大4時間で消灯 |
| タンクの水 | フロート式水位スイッチ | ポンプの動作禁止 | タンクが空ならポンプは絶対に動かない |
植物に必要なものは? 目標の温度、湿度、光
ルールを書く前に、何を目標にするかを知っておく必要があります。ほとんどの植物は昼と夜で異なる温度を好み、夜に少し涼しくなると植物は休めます。湿度が長時間85%を超えると葉に水滴がつき、カビやべと病などの病気が一気に広がります。だからこそ温室のファンは、冷やすためだけでなく湿気を逃がすためにも回ります。光については、ほとんどの野菜の苗は1日14〜16時間を好みますが、少なくとも6〜8時間の暗い時間も必要です。LEDを一晩中つけたままにすると、植物にとってはよいことより害のほうが大きくなります。下の値はよい出発点の目安なので、育てる植物に合わせて調整してください。
| 植物 | 昼 | 夜 | 相対湿度 | メモ |
|---|---|---|---|---|
| トマト | 21〜27 °C | 15〜18 °C | 60〜75% | 32 °Cを超えると花が落ちることがある |
| ピーマン | 21〜29 °C | 16〜20 °C | 60〜75% | トマトより寒さに弱い |
| レタスとホウレンソウ | 15〜20 °C | 10〜15 °C | 60〜70% | 暑さと長い日照で早くとう立ちする |
| イチゴ | 18〜24 °C | 10〜13 °C | 60〜75% | 湿度が高いと灰色かび病のリスクが高まる |
| 苗と発芽 | 20〜25 °C | 18〜20 °C | 70〜85% | 気温より地温のほうが重要 |
模型の温室を作る
いちばん簡単で安い箱はふた付きの透明なプラスチック収納ボックスです。40〜60リットルのボックスなら鉢を2〜3個置け、防水で、カッターナイフや熱したこて先で簡単に穴をあけられます。よりリアルな見た目にしたいなら、木の角材で骨組みを作り、2〜3 mmの透明なアクリル板(プレキシガラス)かPETG板を張ります。切妻屋根の温室は発表の場でずっと見栄えがします。大きさは50×35×40 cmくらいから始めるのがよいでしょう。どの箱を選ぶにしても、ふたか側面の1つは開くようにしておく必要があります。水やり、センサーの移動、ケーブルの整理のために中に手を入れることになるからです。
- 電子部品は外に:温室の中は湿度が高く、朝には壁に水滴がつきます。Arduino、リレーモジュール、電源は温室の外の別の箱に入れ、中に入れるのはセンサー、ファン、ヒーター、ポンプのホース、LEDだけにします。
- 換気:暖かい空気は上に昇るので、ファンは短い側の壁の上部に、空気を外へ吹き出す向きで取り付けます。反対側の壁の下部にファンと同じ大きさの吸気口を切り抜き、防虫ネットでふさぎます。こうすると新鮮な空気が下から入り、暖かく湿った空気が上から出ていきます。
- ヒーター:ヒーターはアルミ板かタイルの上に載せて床近くに取り付け、プラスチックの壁や植物から少なくとも5 cm離します。暖かい空気は上昇しながら温室全体を循環します。
- LED照明:LEDテープはふたの内側、植物の15〜30 cm上に貼ります。薄いアルミ製のフレームにテープを取り付けると放熱がよくなり、時間がたっても粘着テープがはがれません。
- センサー:DHT22は、直射日光やLEDの光が当たらない植物の高さに置きます。土壌水分センサーは鉢の縁から離し、根のあたりに埋めます。LDRにはLEDが見えてはいけないので、温室の外の屋根の上に空へ向けて置きます。
- 水タンク:ポンプとフロートスイッチには1〜2リットルの容器で十分です。鉢の下には受け皿を置きます。余分な水は箱の底を水浸しにせず、受け皿にたまります。
部品リストと費用
| 部品 | 数量 | 役割 | 価格帯 |
|---|---|---|---|
| Arduino Uno R3またはESP32 DevKit | 1 | 温室の頭脳 | 中程度 |
| DHT22(AM2302)温湿度モジュール | 1 | 気温と湿度 | 安い |
| 静電容量式の土壌水分センサー(v1.2) | 1(鉢1つにつき1個) | 土壌水分 | 安い |
| LDR(GL5528)と10 kΩの抵抗 | 1 + 1 | 周囲の明るさ | とても安い |
| フロート式水位スイッチ | 1 | タンクが空のときにポンプを止める | 安い |
| 4チャンネル5 Vリレーモジュール(フォトカプラ付き、JD-VCCあり) | 1 | ファン、ヒーター、ポンプ | 安い |
| ロジックレベルのNチャネルMOSFET(IRLZ44N)、220 Ωと100 kΩの抵抗 | 1セット | LEDテープの明るさ(PWM) | とても安い |
| 3〜6 Vの小型水中ポンプと1 mのシリコンホース | 1 | 水やり | 安い |
| 12 V・80 mmのPC用ファン | 1〜2 | 換気と除湿 | 安い |
| 12 VのPTCヒーター(10〜30 W)と温度ヒューズ | 1 + 1 | 加温 | 安い |
| 12 VのLEDテープ(植物育成用または昼光色)、約1 m | 1 | 補光 | 安い |
| I2C接続の16x2 LCD | 1 | 状態の表示 | 安い〜中程度 |
| 12 V・5 AのACアダプターとLM2596降圧コンバーター | 1 + 1 | 電源 | 中程度 |
| 1N5819または1N4007ダイオード、470 µFのコンデンサ、ネジ端子、ジャンパーワイヤー | ― | 保護と接続 | とても安い |
| ふた付きの透明な収納ボックスまたはアクリル板、鉢、受け皿 | ― | 温室の本体 | 中程度 |
価格は販売店によって大きく異なるので、正確な金額ではなくおおよその予算を示します。ボードを除いた基本版(DHT22、土壌水分センサー、LDR、2チャンネルリレー、ポンプ、5 Vファン)は、たいてい25〜40ドルほどです。ヒーター、LEDテープ、LCD、12 VのACアダプター、LM2596、フロートスイッチ、箱を含む完全版は、たいてい約70〜120ドルになります。予算の大部分はボード、ACアダプター、ディスプレイ、箱にかかります。Arduinoスターターキットを持っていれば、ボード、ブレッドボード、ワイヤー、LDR、抵抗、そして多くの場合LCDもそろっています。予算が厳しいなら、ヒーターとLEDは第2段階に回しましょう。水やりと換気だけでも、十分に意味のある動くプロジェクトになります。
センサー:DHT22、土壌水分センサー、LDR
温室にはDHT22(AM2302)をおすすめします。DHT11より少し高価ですが、温度は−40〜80 °Cを±0.5 °C、湿度は0〜100%を約±2%で測れます。DHT11は0〜50 °Cの範囲で±2 °Cの精度しかなく、90%を超える湿度は測れません。温室は朝に湿度が90%を超え、冬には氷点下になることもあるので、DHT22のほうがずっと適しています。このセンサーは2秒に1回までしか読み取れないため、コードでは2.5秒ごとに読み取ります。日光やLEDの光が直接当たる場所にセンサーを置くと本体が温まり、気温より数度高い値を示します。穴のあいた白いカバーで日陰を作れば防げます。配線とライブラリの詳細はDHT11・DHT22温湿度センサーの記事にあります。

土壌水分には静電容量式のセンサーを選んでください。安価な抵抗式のセンサー(YL-69、FC-28)は2本の金属の電極が土に直接触れていて、常に電源を入れておくと電気分解のため数週間で腐食します。何か月も動かすシステムには向いていません。静電容量式のセンサーは電極がコーティングされているので、金属が土に触れません。出力はアナログで、土が湿るほど値が下がります。Unoでは、空気中に置いたセンサーはおおよそ500〜600、水に浸すと約250〜300を示します。この値はセンサーごとに異なるので、メインのコードを書き込む前に、自分のセンサーの空気中と水中での値をシリアルモニタで読み取り、SOIL_DRY_RAWとSOIL_WET_RAWの定数に書き込みます。コードはこの2つの値をもとにmap()で0〜100%に変換します。詳しくは土壌水分センサーの記事を参照してください。
光には、キットに入っているLDR(GL5528)で十分です。LDRは10 kΩの抵抗と組み合わせて分圧回路として配線します:5V → LDR → A1 → 10 kΩ → GND。光が強くなるとLDRの抵抗が下がり、A1の値が上がります。コードはこの値も、暗いところと日なたで測った2つの数値をもとに0〜100%に変換します。LDRは精密な照度計ではありませんが、「暗くなった? 曇っている? 晴れている?」という問いには確実に答えられます。より正確に光を測りたいなら、BH1750などのI2C光センサーに切り替えられます。分圧の計算には分圧計算ツールを、詳しくはLDR光センサーの記事を使ってください。
アクチュエーター:リレーか、MOSFETか?
Arduinoのピンが供給できるのは約20 mAだけですが、ファン、ヒーター、ポンプ、LEDテープは数百ミリアンペア、ときには数アンペアを消費します。そのため、どの負荷もリレーかMOSFETを通して駆動します。リレーは機械式のスイッチで、まったく別の回路にある負荷をAC・DCを問わずオン・オフできますが、できるのはオンかオフだけで、毎回カチッと音がし、接点の寿命にも限りがあります。MOSFETは音のしない電子スイッチで、DCの負荷にしか使えませんが、毎秒数百回もスイッチングできるので、PWMで明るさや速度を調整できます。このプロジェクトでは、ヒーター、ファン、ポンプに4チャンネルのリレーモジュールを、LEDテープの明るさにMOSFETを使います。
| 負荷 | 代表的な値 | 駆動方法 | 理由 |
|---|---|---|---|
| 12 VのPTCヒーター | 10〜30 W、約1〜2.5 A | リレー(IN2) | 電流が大きく、オン・オフだけで足りる |
| 12 V・80 mmのファン | 0.1〜0.2 A | リレー(IN1)またはMOSFET | リレーは簡単、MOSFETならPWMで速度も制御できる |
| 3〜6 Vの小型水中ポンプ | 0.1〜0.25 A、起動時はそれ以上 | リレー(IN3)またはダイオード付きのMOSFET | 短く区切って動かす。モーターのノイズを抑える必要がある |
| 12 VのLEDテープ(5050、60 LED/m) | 1 mあたり約1.2 A | ロジックレベルMOSFET | PWMによる明るさの制御と静かな動作 |

小型の水中ポンプは、ホースの高さにもよりますが毎分約1〜1.5リットルを送るので、3秒の1回分で鉢に50〜75 mLの水が入ります。そのため、ポンプを「土が湿るまで」連続で動かすのではなく、短く区切って動かします。3秒水をやり、水がしみ込むまで60秒待ち、もう一度測ります。水がセンサーに届く前に再び読み取ると、土はまだ乾いて見え、鉢があふれるまでポンプが動き続けてしまいます。空運転したポンプは熱を持ち、数分で壊れます。タンクのフロートスイッチは、水がなくなるとポンプを止めます。モーターは止まるたびに、コイルが逆向きの電圧スパイクを発生させます。ポンプの端子間に逆向きに入れたダイオード(1N5819または1N4007、帯のある側を+側に)が、このノイズからリレーの接点とArduinoの両方を守ります。ポンプの選び方と駆動方法はミニウォーターポンプの記事で解説しています。
加温には12 VのPTCヒーターを使います。PTC(正の温度係数)セラミック素子は温まるほど抵抗が上がるので、表面温度は一定の値で自然に頭打ちになります。むき出しで赤熱する抵抗線よりずっと安全です。プラスチックの箱には、表面温度が約80 °Cに制限された低温タイプを選んでください。24 Wのヒーターは、P = V × Iより12 Vで2 Aを消費します。冷えている最初の数秒は、電流がやや多くなります。照明には、赤と青の植物育成用LEDテープか昼光色のLEDテープを使えます。植物は光合成に主に赤(約660 nm)と青(約450 nm)の光を使います。テープはIRLZ44NなどのロジックレベルMOSFETで駆動します。ゲートは220 Ωの抵抗を通してD9に、100 kΩの抵抗を通してGNDにつなぎます。テープの+は12 Vに、−はMOSFETのドレインに、ソースはGNDにつなぎます。IRF520などの一般的なMOSFETモジュールは、5 Vのゲート電圧では完全にオンにならず、数アンペアの負荷で発熱します。詳しくはMOSFETで大電流を駆動するの記事を参照してください。電流と電力の計算にはオームの法則計算ツールを使えます。
電源の計画:12 Vと5 Vのライン
スマート温室で最もよくある問題(「ポンプが動くとArduinoがリセットする」「LCDに変な文字が出る」)は電源が原因です。最もすっきりした解決策は、1台の12 V・5 AのACアダプターから2系統のラインを取ることです。12 Vのラインはヒーター、ファン、LEDテープに直接給電し、Arduino UnoのDCジャックにも入ります。同じ12 Vは5 Vに設定したLM2596降圧コンバーターにも入り、リレーのコイル、ポンプ、LCDはこの5 Vのラインで動きます。リレーモジュールのJD-VCCジャンパーを外すと、コイルの電流はすべてLM2596から供給され、Arduino側はフォトカプラのLEDを光らせるだけになります。センサー(DHT22、土壌水分センサー、LDRの分圧回路)はArduino自身の5Vピンから給電します。analogRead()はボード自身の5 Vを基準に測るので、アナログセンサーを同じ電圧から給電すると読み取り値が安定します。ACアダプター、LM2596、ArduinoのGNDは必ずすべてつないでください。
| 負荷 | ライン | おおよその電流 |
|---|---|---|
| PTCヒーター(24 W) | 12 V | 2 A(冷えているときの最初の瞬間はそれ以上) |
| LEDテープ(1 m) | 12 V | 1〜1.2 A |
| 80 mmのファン2台 | 12 V | 0.2〜0.4 A |
| Arduino Unoとセンサー(DCジャック経由) | 12 V | 約0.06〜0.08 A |
| LM2596の入力(5 V側が1 Aを消費しているとき) | 12 V | 約0.5 A |
| リレーモジュール(3個のリレーがオン) | 5 V | 0.2〜0.25 A |
| 小型の水中ポンプ | 5 V | 0.1〜0.25 A(起動時はそれ以上) |
| I2C LCD(バックライト点灯) | 5 V | 20〜30 mA |
すべてが同時に動いても合計電流は約4〜4.5 Aなので、12 V・5 A(60 W)のACアダプターなら十分な余裕があります。実際には、ヒーターと温度用のファンはしきい値が大きく離れているため、同時に動くことはありません。LM2596を回路につなぐ前に、テスターで出力を測り、基板の半固定抵抗で5.0 Vに合わせてください。出荷時の状態では、12 Vがほぼそのまま出てくることがあります。コードを書き込むときはUSBから、実際に使うときはDCジャックからArduinoに給電できます。Unoなら両方を同時につないでも問題ありません。詳しくはArduinoの電源の取り方のガイドを参照してください。
配線とピンの表
下の表は、このガイドの3つのプログラムで使うピンを示しています。Unoでは、LEDテープを駆動するMOSFETをPWM対応のD9につなぎました。UnoでPWMが使えるのはD3、D5、D6、D9、D10、D11だけです。フロートスイッチとリレーにはPWMは不要です。LCDはI2Cバス(A4とA5)だけを使うので、使うピンはごくわずかです。UnoではA2、A3といくつかのデジタルピンが空いているので、2つ目の土壌センサーやボタンに使えます。ESP32では、Wi-Fiがオンでも動き続けるADC1のピン(GPIO 34と35)にアナログセンサーをつなぎました。自分のボードのピン配置はピン配置ツールで確認できます。
| 接続 | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| DHT22のDATA | D2 | GPIO 4 | GP15 |
| フロートスイッチ(もう一方の線はGNDへ) | D3 | GPIO 13 | GP14 |
| リレーIN3:ポンプ | D4 | GPIO 25 | GP18 |
| リレーIN1:ファン | D7 | GPIO 26 | GP16 |
| リレーIN2:ヒーター | D8 | GPIO 27 | GP17 |
| MOSFETのゲート(220 Ω経由):LED | D9 | GPIO 18 | GP19 |
| 土壌水分センサーのAOUT | A0 | GPIO 34 | GP26 |
| LDR分圧回路の中点 | A1 | GPIO 35(3V3からの分圧) | GP27(3V3からの分圧) |
| I2C LCDのSDA / SCL | A4 / A5 | GPIO 21 / 22 | GP4 / GP5 |
| センサーの電源 | 5V | 3V3 | 3V3 |
しきい値とヒステリシス:リレーがカチカチと切り替わり続けるのはなぜ?
1つのしきい値でファンを制御する場合を考えてみましょう。「温度が30 °Cを超えたら回し、そうでなければ止める」。温度が29.9〜30.1 °Cの間で揺れると(センサーのノイズでよく起こります)、リレーは読み取りのたびにオン・オフします。リレーの接点もファンのモーターも消耗し、ヒーターの場合はオンになるたびに配線とACアダプターに無駄な負担がかかります。解決策はヒステリシスです。オンとオフに別々のしきい値を使います。ファンは30 °Cで回り始めますが、止まるのは温度が27 °Cまで下がったときだけです。その間の3度の不感帯では、システムは直前の状態を保ちます。給湯器の室温サーモスタットや冷蔵庫も同じ原理で動いています。
| 制御 | オンになる | オフになる | 不感帯 |
|---|---|---|---|
| ファン(温度) | ≥ 30 °C | ≤ 27 °C | 3 °C |
| ファン(湿度) | ≥ 85% | ≤ 75% | 10% |
| ヒーター | ≤ 16 °C | ≥ 19 °C | 3 °C |
| 水やり | 土壌 ≤ 35% | 土壌 ≥ 50% | 15% |
| LED照明 | 明るさ ≤ 25% | 明るさ ≥ 35%または4時間後 | 10% |
不感帯の幅はトレードオフです。狭すぎるとリレーは依然として頻繁に切り替わり、広すぎると温度が目標から離れすぎます。温度は約2〜3 °C、湿度は8〜10%、土壌水分は10〜15%から始めるのがよいでしょう。また、ヒーターがオフになる19 °Cとファンが回り始める30 °Cの間を大きく空けたので、ヒーターと温度用のファンが互いに打ち消し合うことはありません。コードでは、制御ごとに書き直さなくて済むように、ヒステリシスを小さなhysteresis()関数にまとめています。オンのしきい値がオフのしきい値より大きければ「上がったらオン」(ファン)、小さければ「下がったらオン」(ヒーター、水やり、LED)という意味になります。センサーデータの急な変動もならしたい場合は、センサーデータのフィルタリングの記事をご覧ください。
コードの構成:測る、判断する、動かす、表示する
温室のコードで最も重要なルールは、どの処理もほかの処理を待たないことです。ポンプの後にdelay(60000)で丸1分待つと、その間ファンは回らず、表示も更新されません。そのためコードは、millis()で時間を測る小さな関数でできていて、それぞれが1つの仕事だけを受け持ちます。loop()はそれらを毎秒数千回順番に呼び出し、各関数は自分の出番が来たときだけ処理を行います。水やりは3つの状態を持つ小さなステートマシンです。待機(IDLE)、水やり(PUMPING)、水をしみ込ませる(SOAKING)の3つです。設定はすべてコードの先頭に定数としてまとめてあるので、しきい値を変えるためにコードの中を探し回る必要はありません。この考え方の背景はmillis()とdelay()の違いとArduinoのステートマシンの記事にあります。コードには次の安全対策も含まれています。
- 安全な起動:pinMode()を呼ぶ前にリレーのピンへ「オフ」の値を書き込むので、Arduinoの起動中にリレーが一瞬でもカチッとオンになることはありません。
- センサーの故障:DHT22を3回続けて読み取れないと、コードは安全な状態に切り替わります。ヒーターをオフにし、ファンを回し、LCDに「Sensor hatasi」(トルコ語で「センサーエラー」)と表示します。温度がわからないシステムが、ヒーターをオンのままにすることがあってはなりません。
- タンクの保護:フロートスイッチがタンクは空だと知らせると、ポンプはまったく起動せず、動いている場合はすぐに止まります。
- あふれ防止:1回の水やりサイクルで6回区切って水をやっても土壌水分が50%に達しない場合、センサーが土から抜けたかホースが外れた可能性があります。コードは水やりを止め、「Sulama hatasi」(トルコ語で「水やりの異常」)と表示します。土が再び湿ると、警告は自動で消えます。
- データの記録:コードは10秒ごとに読み取り値をカンマ区切り(CSV)の1行としてシリアルモニタに書き出します。この行をコピーすれば、Excelでグラフを描けます。
ステップごとの組み立てとテスト
- 箱を準備します。ファン、吸気口、ホース、ケーブル用の穴を切り抜き、電子部品の箱を温室の外に置きます。
- 電源のラインを用意します。12 VのACアダプターをネジ端子につなぎ、テスターを見ながらLM2596を5.0 Vに合わせ、GNDをすべてつなぎます。
- I2C LCDをつなぎ、何か文字を表示させます。何も表示されない場合は、I2C LCDディスプレイの記事にあるスキャナーのスケッチとコントラスト調整用の半固定抵抗を試してください。
- センサーを1つずつ追加し、シリアルモニタでテストします。DHT22の読み取り値、土壌センサーの空気中と水中での生の値、LDRの暗いところと日なたでの値を書き留めます。
- まず負荷をつながずに、LEDを見てカチッという音を聞きながらリレーをテストします。次にファン、ポンプ(タンクを満たした状態で)、最後にヒーターをつなぎます。
- LEDテープをMOSFET経由でつなぎ、analogWrite()の値を変えて明るさが変わるのを確認します。
- メインのコードを書き込み、キャリブレーションの値を定数に入力して、見守りながら数時間システムを動かします。水やりの間違い、カチカチ鳴り続けるリレー、熱くなりすぎる部品はないでしょうか?
Arduino Uno用の完全なコード
コードをコンパイルするには、ライブラリマネージャからAdafruitのDHT sensor library(依存ライブラリのAdafruit Unified Sensorも一緒に)と、Frank de BrabanderのLiquidCrystal I2Cライブラリをインストールします。WireはArduino IDEに付属しています。LCDのアドレスは通常0x27で、モジュールによっては0x3Fです。書き込む前に、測ったキャリブレーションの値をSOIL_DRY_RAW、SOIL_WET_RAW、LDR_DARK_RAW、LDR_BRIGHT_RAWの定数に書き込みます。フロートスイッチが水で閉じるのではなく開くタイプなら、TANK_OKをHIGHにします。リレーモジュールがアクティブHIGHなら、RELAY_ONとRELAY_OFFの値を入れ替えてください。
1#include <Wire.h>
2#include <LiquidCrystal_I2C.h>
3#include <DHT.h>
4
5// Pinler / Pins
6const byte DHT_PIN = 2, FLOAT_PIN = 3, PUMP_RELAY = 4, FAN_RELAY = 7, HEATER_RELAY = 8;
7const byte LED_PIN = 9; // MOSFET gate (PWM)
8const byte SOIL_PIN = A0, LDR_PIN = A1;
9
10// Kalibrasyon: kendi ölçümlerini yaz / Calibration: write your own readings
11const int SOIL_DRY_RAW = 520, SOIL_WET_RAW = 260; // havada ve suda / in air and in water
12const int LDR_DARK_RAW = 60, LDR_BRIGHT_RAW = 900; // karanlık ve güneş / dark and sunlight
13
14// Eşikler (açma, kapama) / Thresholds (on, off)
15const float FAN_ON_C = 30, FAN_OFF_C = 27; // sıcaklıkta fan / fan on heat
16const float HUM_ON = 85, HUM_OFF = 75; // nemde fan / fan on humidity
17const float HEAT_ON_C = 16, HEAT_OFF_C = 19; // ısıtıcı / heater
18const float SOIL_DRY = 35, SOIL_OK = 50; // sulama başla, dur / start, stop watering
19const float DARK_ON = 25, DARK_OFF = 35; // ışık % / light %
20
21// Zamanlar / Timing
22const unsigned long READ_MS = 2500; // DHT22: en az 2 sn / at least 2 s
23const unsigned long PUMP_MS = 3000; // tek sulama darbesi / one watering pulse
24const unsigned long SOAK_MS = 60000; // suyun yayılması / let the water soak in
25const byte MAX_PULSES = 6; // fazlası arıza / more means a fault
26const unsigned long EXTRA_LIGHT_MS = 4UL * 3600000UL; // günü 4 saat uzat / extend the day by 4 h
27const byte LED_LEVEL = 200; // LED parlaklığı 0-255 / LED brightness 0-255
28const byte RELAY_ON = LOW, RELAY_OFF = HIGH; // aktif LOW röle / active-LOW relay
29const byte TANK_OK = LOW; // şamandıra suyla kapanır / float closes with water
30
31DHT dht(DHT_PIN, DHT22);
32LiquidCrystal_I2C lcd(0x27, 16, 2);
33
34float tempC = NAN, hum = NAN, soil = 0, light = 0;
35byte dhtErrors = 0, pulses = 0, page = 0;
36bool fanByTemp = false, fanByHum = false, fanOn = false, heaterOn = false;
37bool needWater = false, pumpFault = false, isDark = false, ledOn = false, tankOk = true;
38unsigned long lastRead = 0, lastLcd = 0, lastLog = 0, pumpAt = 0, darkSince = 0;
39
40enum PumpState { IDLE, PUMPING, SOAKING };
41PumpState pumpState = IDLE;
42
43// onAt > offAt: yükselince aç (fan) / turn on when rising (fan)
44// onAt < offAt: düşünce aç (ısıtıcı) / turn on when falling (heater)
45bool hysteresis(bool state, float value, float onAt, float offAt) {
46 if (onAt > offAt) {
47 if (value >= onAt) return true;
48 if (value <= offAt) return false;
49 } else {
50 if (value <= onAt) return true;
51 if (value >= offAt) return false;
52 }
53 return state; // ölü bölgede durumu koru / keep state in the dead band
54}
55
56void setRelay(byte pin, bool on) {
57 digitalWrite(pin, on ? RELAY_ON : RELAY_OFF);
58}
59
60float readPercent(byte pin, int raw0, int raw100) {
61 long sum = 0;
62 for (byte i = 0; i < 10; i++) sum += analogRead(pin); // 10 ölçüm ortalaması / average of 10
63 long pct = map(sum / 10, raw0, raw100, 0, 100);
64 return constrain(pct, 0, 100);
65}
66
67void setup() {
68 Serial.begin(9600);
69 setRelay(PUMP_RELAY, false); pinMode(PUMP_RELAY, OUTPUT); // açılışta kapalı / off at boot
70 setRelay(FAN_RELAY, false); pinMode(FAN_RELAY, OUTPUT);
71 setRelay(HEATER_RELAY, false); pinMode(HEATER_RELAY, OUTPUT);
72 pinMode(LED_PIN, OUTPUT);
73 pinMode(FLOAT_PIN, INPUT_PULLUP);
74 dht.begin();
75 lcd.init();
76 lcd.backlight();
77 lcd.print(F("BlueGrays Sera"));
78 Serial.println(F("sn,sicaklik,nem,toprak,isik,fan,isitici,pompa,led")); // CSV başlığı / CSV header
79 delay(2000); // DHT22 açılış süresi / DHT22 start-up time
80 readSensors();
81 lastRead = millis();
82}
83
84void loop() {
85 unsigned long now = millis();
86 if (now - lastRead >= READ_MS) {
87 lastRead = now;
88 readSensors();
89 }
90 controlClimate();
91 controlLight(now);
92 controlPump(now);
93 updateLcd(now);
94 logCsv(now);
95}
96
97// 1) Ölç / Measure
98void readSensors() {
99 float t = dht.readTemperature();
100 float h = dht.readHumidity();
101 if (isnan(t) || isnan(h)) {
102 if (dhtErrors < 255) dhtErrors++; // okuma hatası / read error
103 } else {
104 tempC = t;
105 hum = h;
106 dhtErrors = 0;
107 }
108 soil = readPercent(SOIL_PIN, SOIL_DRY_RAW, SOIL_WET_RAW);
109 light = readPercent(LDR_PIN, LDR_DARK_RAW, LDR_BRIGHT_RAW);
110 tankOk = digitalRead(FLOAT_PIN) == TANK_OK;
111}
112
113// 2) Fan ve ısıtıcı / Fan and heater
114void controlClimate() {
115 if (dhtErrors >= 3) { // sensör yok: güvenli durum / no sensor: safe state
116 heaterOn = false;
117 fanByTemp = true;
118 } else {
119 fanByTemp = hysteresis(fanByTemp, tempC, FAN_ON_C, FAN_OFF_C);
120 fanByHum = hysteresis(fanByHum, hum, HUM_ON, HUM_OFF);
121 heaterOn = hysteresis(heaterOn, tempC, HEAT_ON_C, HEAT_OFF_C);
122 }
123 fanOn = fanByTemp || fanByHum;
124 setRelay(FAN_RELAY, fanOn);
125 setRelay(HEATER_RELAY, heaterOn);
126}
127
128// 3) LED aydınlatma / LED grow light
129void controlLight(unsigned long now) {
130 bool wasDark = isDark;
131 isDark = hysteresis(isDark, light, DARK_ON, DARK_OFF);
132 if (isDark && !wasDark) darkSince = now; // akşam oldu / dusk detected
133 ledOn = isDark && (now - darkSince < EXTRA_LIGHT_MS);
134 analogWrite(LED_PIN, ledOn ? LED_LEVEL : 0);
135}
136
137// 4) Sulama: darbe + bekleme / Watering: pulse + soak
138void controlPump(unsigned long now) {
139 needWater = hysteresis(needWater, soil, SOIL_DRY, SOIL_OK);
140 if (!needWater) {
141 pulses = 0;
142 pumpFault = false; // toprak ıslandı / soil is wet again
143 }
144 switch (pumpState) {
145 case IDLE:
146 if (needWater && tankOk && !pumpFault) {
147 if (pulses >= MAX_PULSES) {
148 pumpFault = true; // nem yükselmiyor / moisture is not rising
149 } else {
150 pumpState = PUMPING;
151 pumpAt = now;
152 pulses++;
153 }
154 }
155 break;
156 case PUMPING:
157 if (now - pumpAt >= PUMP_MS || !tankOk) {
158 pumpState = SOAKING;
159 pumpAt = now;
160 }
161 break;
162 case SOAKING:
163 if (now - pumpAt >= SOAK_MS) pumpState = IDLE;
164 break;
165 }
166 setRelay(PUMP_RELAY, pumpState == PUMPING);
167}
168
169// 5) LCD: üç sayfa / three pages
170void updateLcd(unsigned long now) {
171 if (now - lastLcd < 3000) return;
172 lastLcd = now;
173 page = (page + 1) % 3;
174 lcd.clear();
175 if (page == 0) {
176 lcd.print(F("Sicaklik: "));
177 if (isnan(tempC)) lcd.print(F("--")); else lcd.print(tempC, 1);
178 lcd.print('C');
179 lcd.setCursor(0, 1);
180 lcd.print(F("Hava nemi: %"));
181 if (isnan(hum)) lcd.print(F("--")); else lcd.print(hum, 0);
182 } else if (page == 1) {
183 lcd.print(F("Toprak: %"));
184 lcd.print(soil, 0);
185 lcd.setCursor(0, 1);
186 lcd.print(F("Isik: %"));
187 lcd.print(light, 0);
188 } else {
189 lcd.print(F("F:")); lcd.print(fanOn);
190 lcd.print(F(" I:")); lcd.print(heaterOn);
191 lcd.print(F(" P:")); lcd.print(pumpState == PUMPING);
192 lcd.print(F(" L:")); lcd.print(ledOn);
193 lcd.setCursor(0, 1);
194 if (dhtErrors >= 3) lcd.print(F("Sensor hatasi"));
195 else if (!tankOk) lcd.print(F("Depo bos!"));
196 else if (pumpFault) lcd.print(F("Sulama hatasi"));
197 else lcd.print(F("Sistem normal"));
198 }
199}
200
201// 6) Seri port CSV kaydı / Serial CSV log
202void logCsv(unsigned long now) {
203 if (now - lastLog < 10000) return;
204 lastLog = now;
205 Serial.print(now / 1000); Serial.print(',');
206 Serial.print(tempC, 1); Serial.print(',');
207 Serial.print(hum, 0); Serial.print(',');
208 Serial.print(soil, 0); Serial.print(',');
209 Serial.print(light, 0); Serial.print(',');
210 Serial.print(fanOn); Serial.print(',');
211 Serial.print(heaterOn); Serial.print(',');
212 Serial.print(pumpState == PUMPING); Serial.print(',');
213 Serial.println(ledOn);
214}| 関数 | 役割 | 頻度 |
|---|---|---|
| readSensors()(センサーの読み取り) | DHT22、土壌水分、LDR、フロートスイッチを読み取り、10回の読み取りの平均を使ってアナログ値を0〜100%に変換 | 2.5秒ごと |
| controlClimate()(環境の制御) | 温度または湿度のヒステリシスでファンを、温度のヒステリシスでヒーターを駆動。センサーが故障したら安全な状態に切り替え | 毎周回 |
| controlLight()(照明の制御) | 暗くなるとPWMでLEDを点灯し、最大4時間後に消灯 | 毎周回 |
| controlPump()(ポンプの制御) | 水やりのステートマシン:3秒水をやり、60秒待ち、再び測る。タンクの保護とあふれ防止付き | 毎周回 |
| updateLcd()(LCDの更新) | 空気の読み取り値、土壌水分と明るさ、機器の状態の3ページを順に表示 | 3秒ごと |
| logCsv()(CSVの記録) | 読み取り値と状態をCSVの1行としてシリアルモニタに書き出す | 10秒ごと |
LCDの1ページ目には温度と空気の湿度、2ページ目には土壌水分と明るさ、3ページ目には機器の状態が表示されます。「F:1 I:0 P:0 L:1」は、ファンが動いていて、ヒーター(I = ısıtıcı、トルコ語で「ヒーター」)とポンプがオフ、LEDが点灯していることを意味します。下の行には「Sistem normal」(正常)、「Depo bos!」(タンクが空)、「Sulama hatasi」(水やりの異常)、「Sensor hatasi」(センサーエラー)のいずれかのメッセージが表示されます。これらの文字列はコードの中で簡単に翻訳できます。このコードをコンパイルすると、Unoのプログラムメモリの約31%、RAMの25%を使うので、2つ目の土壌センサー、ブザー、RTC時計モジュールを追加する余裕は十分にあります。Unoには本物の時計がないので、LEDのルールは「日没後最大4時間」として動きます。これは冬の短い日を延ばすのによい方法です。毎日決まった時刻にLEDを点灯させたいなら、RTCモジュールを追加するか、下のESP32版に移行してください。
ESP32でスマホから監視と水やり

UnoにはWi-Fiがありません。温室の状態をスマホで見たり、離れた場所から水やりをしたりしたいなら、頭脳をESP32に替えましょう。下のコードは同じ制御ロジックをESP32に移し、小さなWebサーバーを起動します。スマホを同じWi-Fiネットワークにつなぎ、シリアルモニタに表示されたIPアドレスをブラウザで開くと、温度、湿度、土壌水分、明るさ、タンクの状態、各機器のオン・オフが表示されます。ページは10秒ごとに自動で更新されます。Water now(今すぐ水やり)ボタンを押すと、タンクに水があればポンプが3秒間1回だけ動きます。起動時にESP32はインターネット(NTP)から時刻を取得するので、LEDは06:00〜20:00の間で、しかも外が暗いときだけ点灯し、植物は夜に休めます。
1#include <WiFi.h>
2#include <WebServer.h>
3#include <DHT.h>
4#include <time.h>
5
6const char* ssid = "AgAdi"; // Wi-Fi adın / your Wi-Fi name
7const char* password = "Sifre123"; // Wi-Fi şifren / your Wi-Fi password
8
9// Pinler / Pins
10const int DHT_PIN = 4, FLOAT_PIN = 13, PUMP_RELAY = 25, FAN_RELAY = 26, HEATER_RELAY = 27;
11const int LED_PIN = 18; // MOSFET gate (PWM)
12const int SOIL_PIN = 34, LDR_PIN = 35; // ADC1: Wi-Fi açıkken çalışır / works with Wi-Fi on
13
14// Kalibrasyon (0-4095): kendin ölç / Calibration (0-4095): measure your own
15const int SOIL_DRY_RAW = 2800, SOIL_WET_RAW = 1200;
16const int LDR_DARK_RAW = 200, LDR_BRIGHT_RAW = 3800;
17
18// Eşikler (açma, kapama) / Thresholds (on, off)
19const float FAN_ON_C = 30, FAN_OFF_C = 27, HUM_ON = 85, HUM_OFF = 75;
20const float HEAT_ON_C = 16, HEAT_OFF_C = 19, SOIL_DRY = 35, SOIL_OK = 50;
21const float DARK_ON = 25, DARK_OFF = 35;
22const int LIGHT_FROM = 6, LIGHT_TO = 20; // LED yalnız 06:00-20:00 / LEDs only 06:00-20:00
23
24const unsigned long READ_MS = 2500, PUMP_MS = 3000, SOAK_MS = 60000;
25const unsigned long EXTRA_LIGHT_MS = 4UL * 3600000UL; // saat yoksa yedek kural / fallback without a clock
26const int MAX_PULSES = 6, LED_LEVEL = 200;
27const int RELAY_ON = LOW, RELAY_OFF = HIGH; // aktif LOW röle / active-LOW relay
28const int TANK_OK = LOW; // şamandıra suyla kapanır / float closes with water
29
30DHT dht(DHT_PIN, DHT22);
31WebServer server(80);
32
33float tempC = NAN, hum = NAN, soil = 0, light = 0;
34int dhtErrors = 0, pulses = 0;
35bool fanByTemp = false, fanByHum = false, fanOn = false, heaterOn = false;
36bool needWater = false, pumpFault = false, isDark = false, ledOn = false, tankOk = true;
37unsigned long lastRead = 0, pumpAt = 0, darkSince = 0;
38
39enum PumpState { IDLE, PUMPING, SOAKING };
40PumpState pumpState = IDLE;
41
42bool hysteresis(bool state, float value, float onAt, float offAt) {
43 if (onAt > offAt) { // yükselince aç / turn on when rising
44 if (value >= onAt) return true;
45 if (value <= offAt) return false;
46 } else { // düşünce aç / turn on when falling
47 if (value <= onAt) return true;
48 if (value >= offAt) return false;
49 }
50 return state;
51}
52
53void setRelay(int pin, bool on) {
54 digitalWrite(pin, on ? RELAY_ON : RELAY_OFF);
55}
56
57float readPercent(int pin, int raw0, int raw100) {
58 long sum = 0;
59 for (int i = 0; i < 16; i++) sum += analogRead(pin); // 16 ölçüm ortalaması / average of 16
60 long pct = map(sum / 16, raw0, raw100, 0, 100);
61 return constrain(pct, 0, 100);
62}
63
64int hourNow() {
65 struct tm t;
66 if (!getLocalTime(&t, 0)) return -1; // saat henüz yok / time not set yet
67 return t.tm_hour;
68}
69
70void readSensors() {
71 float t = dht.readTemperature();
72 float h = dht.readHumidity();
73 if (isnan(t) || isnan(h)) {
74 dhtErrors++;
75 } else {
76 tempC = t;
77 hum = h;
78 dhtErrors = 0;
79 }
80 soil = readPercent(SOIL_PIN, SOIL_DRY_RAW, SOIL_WET_RAW);
81 light = readPercent(LDR_PIN, LDR_DARK_RAW, LDR_BRIGHT_RAW);
82 tankOk = digitalRead(FLOAT_PIN) == TANK_OK;
83}
84
85void controlClimate() {
86 if (dhtErrors >= 3) { // sensör yok: güvenli durum / no sensor: safe state
87 heaterOn = false;
88 fanByTemp = true;
89 } else {
90 fanByTemp = hysteresis(fanByTemp, tempC, FAN_ON_C, FAN_OFF_C);
91 fanByHum = hysteresis(fanByHum, hum, HUM_ON, HUM_OFF);
92 heaterOn = hysteresis(heaterOn, tempC, HEAT_ON_C, HEAT_OFF_C);
93 }
94 fanOn = fanByTemp || fanByHum;
95 setRelay(FAN_RELAY, fanOn);
96 setRelay(HEATER_RELAY, heaterOn);
97}
98
99void controlLight(unsigned long now) {
100 bool wasDark = isDark;
101 isDark = hysteresis(isDark, light, DARK_ON, DARK_OFF);
102 if (isDark && !wasDark) darkSince = now;
103 int h = hourNow();
104 if (h >= 0) ledOn = isDark && h >= LIGHT_FROM && h < LIGHT_TO; // saat kuralı / clock rule
105 else ledOn = isDark && (now - darkSince < EXTRA_LIGHT_MS); // yedek kural / fallback rule
106 analogWrite(LED_PIN, ledOn ? LED_LEVEL : 0);
107}
108
109void controlPump(unsigned long now) {
110 needWater = hysteresis(needWater, soil, SOIL_DRY, SOIL_OK);
111 if (!needWater) {
112 pulses = 0;
113 pumpFault = false;
114 }
115 if (pumpState == IDLE && needWater && tankOk && !pumpFault) {
116 if (pulses >= MAX_PULSES) {
117 pumpFault = true; // nem yükselmiyor / moisture is not rising
118 } else {
119 pumpState = PUMPING;
120 pumpAt = now;
121 pulses++;
122 }
123 } else if (pumpState == PUMPING && (now - pumpAt >= PUMP_MS || !tankOk)) {
124 pumpState = SOAKING;
125 pumpAt = now;
126 } else if (pumpState == SOAKING && now - pumpAt >= SOAK_MS) {
127 pumpState = IDLE;
128 }
129 setRelay(PUMP_RELAY, pumpState == PUMPING);
130}
131
132String row(const char* name, const String& value) {
133 return String("<tr><td>") + name + "</td><td><b>" + value + "</b></td></tr>";
134}
135
136String onOff(bool on) {
137 return on ? "ON" : "OFF";
138}
139
140void handleRoot() {
141 String html = "<!DOCTYPE html><html><head><meta charset='utf-8'>"
142 "<meta name='viewport' content='width=device-width,initial-scale=1'>"
143 "<meta http-equiv='refresh' content='10'><title>BlueGrays Sera</title>"
144 "<style>body{font-family:sans-serif;max-width:420px;margin:auto;padding:12px}"
145 "td{padding:8px;border-bottom:1px solid #ddd}"
146 "a{display:block;margin-top:16px;padding:16px;border-radius:10px;background:#2e7d32;"
147 "color:#fff;text-align:center;text-decoration:none;font-size:18px}</style>"
148 "</head><body><h2>BlueGrays Sera</h2><table>";
149 html += row("Sıcaklık / Temperature", String(tempC, 1) + " °C");
150 html += row("Hava nemi / Humidity", "%" + String(hum, 0));
151 html += row("Toprak nemi / Soil", "%" + String(soil, 0));
152 html += row("Işık / Light", "%" + String(light, 0));
153 html += row("Fan", onOff(fanOn));
154 html += row("Isıtıcı / Heater", onOff(heaterOn));
155 html += row("Pompa / Pump", onOff(pumpState == PUMPING));
156 html += row("LED", onOff(ledOn));
157 html += row("Depo / Tank", tankOk ? "OK" : "BOŞ / EMPTY");
158 if (pumpFault) html += row("Uyarı / Warning", "Sulama hatası / Watering fault");
159 html += "</table><a href='/water'>Şimdi sula / Water now</a></body></html>";
160 server.send(200, "text/html", html);
161}
162
163void handleWater() { // elle tek darbe / one manual pulse
164 if (pumpState == IDLE && tankOk) {
165 pumpState = PUMPING;
166 pumpAt = millis();
167 }
168 server.sendHeader("Location", "/");
169 server.send(303);
170}
171
172void setup() {
173 Serial.begin(115200);
174 setRelay(PUMP_RELAY, false); pinMode(PUMP_RELAY, OUTPUT);
175 setRelay(FAN_RELAY, false); pinMode(FAN_RELAY, OUTPUT);
176 setRelay(HEATER_RELAY, false); pinMode(HEATER_RELAY, OUTPUT);
177 pinMode(LED_PIN, OUTPUT);
178 pinMode(FLOAT_PIN, INPUT_PULLUP);
179 dht.begin();
180 WiFi.begin(ssid, password);
181 while (WiFi.status() != WL_CONNECTED) {
182 delay(500);
183 Serial.print('.');
184 }
185 Serial.println();
186 Serial.println(WiFi.localIP()); // bu adresi telefonda aç / open this address on your phone
187 configTime(3 * 3600, 0, "pool.ntp.org"); // Türkiye UTC+3; kendi saat dilimini yaz / set your own time zone
188 server.on("/", handleRoot);
189 server.on("/water", handleWater);
190 server.begin();
191 readSensors();
192 lastRead = millis();
193}
194
195void loop() {
196 server.handleClient();
197 unsigned long now = millis();
198 if (now - lastRead >= READ_MS) {
199 lastRead = now;
200 readSensors();
201 }
202 controlClimate();
203 controlLight(now);
204 controlPump(now);
205}Unoのコードとの違いは次のとおりです。ESP32のADCは12ビットで0〜4095の値を返すので、キャリブレーションをやり直す必要があります。センサーを3V3から給電するので、生の値も変わります。タイムゾーンはconfigTime()の行で設定します。トルコ(UTC+3)用に3 × 3600秒と書きました。日本(UTC+9、夏時間なし)なら9 × 3600にします。夏時間のある地域では、この行をconfigTzTime()とPOSIX形式のタイムゾーン文字列に置き換えます。たとえばイギリスならconfigTzTime("GMT0BST,M3.5.0/1,M10.5.0", "pool.ntp.org")です。インターネットがない場合や時刻をまだ取得できていない場合、LEDのルールはUnoと同じ「最大4時間」のルールに戻ります。ページにはパスワードがないので、家のネットワークにいる人なら誰でもポンプを動かせてしまいます。WebServerライブラリのauthenticate()関数で簡単なパスワードを付けるのがよい習慣です。ESP32にもLCDを付けたいなら、UnoのコードのupdateLcd()関数をそのままコピーできます。SDAはGPIO 21、SCLはGPIO 22です。
| 方法 | 外出先からのアクセス | 難易度 | 向いている用途 |
|---|---|---|---|
| ESP32のWebサーバー(このガイドのコード) | 不可、同じネットワーク内のみ | 中級 | 学校での発表、インターネットなしのデモ |
| Blynk IoT | 可能 | 簡単 | 既製のメーター、グラフ、通知 |
| Telegramボット | 可能 | 中級 | 「タンクが空」「温度が高すぎる」などの警告 |
| Googleスプレッドシート | 可能 | 中級 | 数週間分のデータ記録とグラフ |
| MQTTとHome Assistant | 可能、設定が必要 | 上級 | 本格的な家と庭の自動化 |
学校での発表には、ESP32のWebサーバーが最も安全な選択です。クラウドサービスに頼らず、会場にインターネットがなくても自分のネットワークで動きます。外出先からも確認したいなら、既製のメーターやグラフがそろったBlynk IoTがいちばん簡単です。タンクが空になったときや温度が危険な値になったときにスマホにメッセージを受け取るにはESP32のTelegramボットのガイドを、レポート用に数週間分のデータを集めるにはESP32とGoogleスプレッドシートを、温室を本格的なホームオートメーションにつなぐにはMQTTとHome Assistantを参照してください。
Raspberry Pi Pico用のMicroPython版
同じ温室をRaspberry Pi PicoとMicroPythonで動かすこともできます。PicoのMicroPythonファームウェアにはDHTドライバが組み込まれているので、追加のファイルをコピーする必要はありません。アナログ入力はGP26、GP27、GP28だけで、read_u16()は0〜65535の値を返します。キャリブレーションの数値はそれに合わせて測ってください。Picoは3.3 Vで動作するので、センサーは3V3から、リレーモジュールはVBUS(USBの5 V)かLM2596のラインから給電します。下のコードにはファン、ヒーター、LED、水やりのルールがすべて入っていて、値をThonnyのシェルに表示します。ファイルをPicoにmain.pyとして保存すれば、パソコンにつながっていなくてもボードが動きます。Wi-Fi対応のPico Wを使う場合は、Pico WのWebサーバーの記事の方法を加えれば、スマホから値を確認できます。
1from machine import Pin, ADC, PWM
2import dht
3import time
4
5sensor = dht.DHT22(Pin(15)) # GP15: DHT22 DATA
6soil_adc = ADC(26) # GP26: toprak nem AOUT / soil moisture AOUT
7ldr_adc = ADC(27) # GP27: LDR bölücü (3V3) / LDR divider (3V3)
8tank = Pin(14, Pin.IN, Pin.PULL_UP) # şamandıra: suyla 0 / float switch: 0 with water
9fan = Pin(16, Pin.OUT, value=1) # aktif LOW röleler kapalı başlar / active-LOW relays start off
10heater = Pin(17, Pin.OUT, value=1)
11pump = Pin(18, Pin.OUT, value=1)
12led = PWM(Pin(19)) # MOSFET gate
13led.freq(1000)
14
15SOIL_DRY_RAW, SOIL_WET_RAW = 50000, 22000 # kendin ölç / measure your own
16LDR_DARK_RAW, LDR_BRIGHT_RAW = 2000, 60000
17EXTRA_LIGHT_MS = 4 * 3600 * 1000 # günü 4 saat uzat / extend the day by 4 h
18
19
20def percent(adc, raw0, raw100):
21 raw = sum(adc.read_u16() for _ in range(10)) // 10
22 pct = (raw - raw0) * 100 // (raw100 - raw0)
23 return max(0, min(100, pct))
24
25
26def hysteresis(state, value, on_at, off_at):
27 if on_at > off_at: # yükselince aç / turn on when rising
28 if value >= on_at:
29 return True
30 if value <= off_at:
31 return False
32 else: # düşünce aç / turn on when falling
33 if value <= on_at:
34 return True
35 if value >= off_at:
36 return False
37 return state # ölü bölge / dead band
38
39
40def relay(pin, on):
41 pin.value(0 if on else 1)
42
43
44t = h = None
45errors = pulses = 0
46fan_t = fan_h = heat_on = need_water = dark = led_on = False
47pump_state, pump_at, dark_since = "idle", 0, 0
48soil = light = 0
49tank_ok = True
50time.sleep(2) # DHT22 açılış süresi / DHT22 start-up time
51last_read = time.ticks_add(time.ticks_ms(), -2500)
52
53while True:
54 now = time.ticks_ms()
55 if time.ticks_diff(now, last_read) >= 2500: # 2,5 sn'de bir ölç / measure every 2.5 s
56 last_read = now
57 try:
58 sensor.measure()
59 t, h = sensor.temperature(), sensor.humidity()
60 errors = 0
61 except OSError:
62 errors += 1 # okuma hatası / read error
63 soil = percent(soil_adc, SOIL_DRY_RAW, SOIL_WET_RAW)
64 light = percent(ldr_adc, LDR_DARK_RAW, LDR_BRIGHT_RAW)
65 tank_ok = tank.value() == 0
66
67 if errors >= 3 or t is None: # güvenli durum / safe state
68 heat_on, fan_t = False, True
69 else:
70 fan_t = hysteresis(fan_t, t, 30, 27)
71 fan_h = hysteresis(fan_h, h, 85, 75)
72 heat_on = hysteresis(heat_on, t, 16, 19)
73 relay(fan, fan_t or fan_h)
74 relay(heater, heat_on)
75
76 was_dark = dark
77 dark = hysteresis(dark, light, 25, 35)
78 if dark and not was_dark:
79 dark_since = now # akşam oldu / dusk detected
80 led_on = dark and time.ticks_diff(now, dark_since) < EXTRA_LIGHT_MS
81 led.duty_u16(50000 if led_on else 0)
82
83 need_water = hysteresis(need_water, soil, 35, 50)
84 if not need_water:
85 pulses = 0
86 print("T", t, "N", h, "Toprak", soil, "Isik", light, "Pompa", pump_state,
87 "Depo OK" if tank_ok else "Depo BOS", "HATA" if pulses >= 6 else "")
88
89 # Sulama: 3 sn darbe, 60 sn bekleme / Watering: 3 s pulse, 60 s soak
90 if pump_state == "idle" and need_water and tank_ok and pulses < 6:
91 pump_state, pump_at = "pump", now
92 pulses += 1
93 elif pump_state == "pump" and (time.ticks_diff(now, pump_at) >= 3000 or not tank_ok):
94 pump_state, pump_at = "soak", now
95 elif pump_state == "soak" and time.ticks_diff(now, pump_at) >= 60000:
96 pump_state = "idle"
97 relay(pump, pump_state == "pump")
98 time.sleep_ms(100)学校や科学コンテストでの発表のコツ
スマート温室はとても人気のあるテーマなので、審査員は似たような模型を何十個も見ています。目立つためには、システムが動くことを見せるだけでなく、測定によって1つの問いに答えることです。科学コンテスト(トルコのTÜBİTAK 4006など)では目的、方法、予想される結果が明確でなければならず、研究コンテスト(TÜBİTAK 2204など)ではデータに基づく比較実験が求められます。この温室に向いた研究の問いの例は次のとおりです。「土壌水分にもとづく水やりは、毎日同じ時刻に水をやる場合と比べてどれだけ水を節約でき、植物の成長にどう影響するか?」
- 2つのグループを用意する:同じ種類で同じ高さの苗を2つのグループに分けます。一方はセンサーのシステムで、もう一方は毎日決まった量を手で水やりします。光と温度は両グループで同じにします。
- 測って記録する:週ごとの水の量(タンクを測ってmL単位で)、草丈(cm)、葉の数を表にまとめます。コードのCSV出力やGoogleスプレッドシートの記録を使えば、温度と湿度のグラフも手間なく手に入ります。
- ブロック図を描く:左にセンサー、中央にArduino、右にアクチュエーターを置き、矢印にはルールの文を書き込みます(「土壌 < 35% → ポンプ」)。審査員はコードを読みませんが、この図は覚えています。
- ヒステリシスを説明する:しきい値が1つのファンとヒステリシス付きのファンが1時間に何回オン・オフするかを示す2つのグラフは、プロジェクトの工学的な側面を審査員に見せるいちばんの方法です。
- ライブデモの台本を書く:手のひらでDHT22を温めてファンを回し、土壌センサーを乾いた鉢に移してポンプが区切って動く様子を見せ、LDRを手で覆ってLEDが点灯するのを見せます。各デモは10〜20秒に収めましょう。
- 実生活と結びつける:温室での水とエネルギーの使用量、湿度の高すぎによる病気、生産者の作業負担といった実際の問題を取り上げ、システムがそのどれを解決するかを数字で示します。
- 予備の計画を用意する:動いているシステムを動画に撮っておきます。予備のワイヤー、予備のポンプ、ACアダプターを持参し、キャリブレーションの値とWi-Fiの設定を紙に書いておきます。
- 審査員の質問に備える:「センサーが故障したらどうなりますか?」「停電したら植物はどうなりますか?」「本物の温室に規模を広げるにはどうしますか?」に短く答えられるようにしておきます。コードにある安全な状態とあふれ防止は、こうした質問へのいちばんの答えです。
よくある失敗と対処法
| 症状 | 考えられる原因 | 対処 |
|---|---|---|
| ポンプが動くとArduinoがリセットする、またはLCDが文字化けする | ポンプとリレーをArduinoの5Vピンから給電している、モーターのノイズ | ポンプとリレーのコイルはLM2596から給電し、GNDをつなぎ、ポンプに逆向きのダイオードと470 µFのコンデンサを付ける |
| リレーがカチカチ鳴り続ける | しきい値が1つだけ、または読み取り値のノイズが多い | オンとオフに別々のしきい値(ヒステリシス)を使い、アナログの読み取り値を平均する |
| ポンプが止まっても水が流れ続ける | サイフォンの原理:ホースの先がタンクの水面より低い | タンクを低い位置に置くか、ホースの最上部に小さな空気穴をあける |
| 土壌水分が常に0%または常に100% | キャリブレーションの値が間違っている、またはAOUTが違うピンにつながっている | センサーの空気中と水中での生の値を測って定数に書き込む |
| LEDが点いたり消えたりする | LDRにLEDの光が見えている | LDRを温室の外に移し、空に向ける |
| DHT22がnanを返す | ピンの間違い、プルアップ抵抗がない、または2秒より短い間隔で読み取っている | ピンを確認し、モジュールを使うか、DATAとVCCの間に10 kΩを入れる |
| 温度が実際より高く表示される | 日光やLEDの光がセンサーに直接当たっている | 穴のあいた白いカバーで日陰を作り、ヒーターから離す |
| 壁に水滴やカビがつく | 湿度が高すぎる、換気が弱すぎる | 湿度のしきい値を下げ、吸気口を大きくし、水やりを減らす |
| ESP32で、Wi-Fiが始まると土壌の読み取り値がおかしくなる | センサーがADC2のピンにつながっている | GPIO 32〜39のADC1のピンに移す |
| ESP32で、リレーがオフにならない | 3.3 VのHIGHではアクティブLOWの5 Vモジュールをオフにできない | JD-VCCを分けるか、3.3 Vでトリガーできるリレーモジュールを使う |
スマート温室プロジェクトは、センサーの読み取り、キャリブレーション、ヒステリシスによる判断、リレーとMOSFETの駆動、電源の計画、ワイヤレスでの監視を1つの作品にまとめたもので、最後には本当に生きているもの、つまり植物を育てます。まずは水やりと換気から始め、各部分を単独でテストしてから、ヒーター、LED、スマホを加えていきましょう。BlueGraysアプリでは、「DHT11で温度と湿度を測定」「LDRで光の強さを測定」「スマート水やりステーション」、そしてDHT22を使う「ESP32温度データロガー(DHT22 + DS1307 RTC + microSD + OLED)」のプロジェクトを、ステップごとの回路図とコード付きで見られます。
よくある質問
スマート温室プロジェクトには何が必要ですか?
基本的なスマート温室なら、Arduino UnoかESP32、DHT22温湿度センサー、静電容量式の土壌水分センサー、LDRと10 kΩの抵抗、リレーモジュール、小型の水中ポンプ、ファンがあれば十分です。完全版では、12 VのPTCヒーター、MOSFETで駆動するLEDテープ、I2C LCD、フロート式水位スイッチ、12 V・5 AのACアダプター、LM2596が加わります。本体には、ふた付きの透明な収納ボックスがいちばん簡単で安上がりです。
スマート温室はどのように動きますか?
スマート温室は、測る、判断する、動かす、表示するというループで動きます。センサーが温度、湿度、土壌水分、明るさを測り、Arduinoがこれらの値をしきい値と比較して、リレーやMOSFETを通してファン、ヒーター、ポンプ、LEDをオン・オフします。結果はLCDに、ESP32を使えばスマホにも表示されます。
温室にはDHT11とDHT22のどちらを使うべきですか?
DHT22を選んでください。DHT11は0〜50 °Cの範囲で±2 °Cの精度しかなく、90%を超える湿度は測れませんが、温室は朝に湿度が90%を超え、冬には氷点下になることもあります。DHT22は温度を−40〜80 °Cで±0.5 °C、湿度を約±2%で測れ、コードもDHT11とほとんど同じです。
Arduinoの温室プロジェクトにはどれくらい費用がかかりますか?
価格は販売店によって異なりますが、ボードを除いた基本版(DHT22、土壌水分センサー、LDR、リレー、ポンプ、ファン)はたいてい25〜40ドルほどです。ヒーター、LEDテープ、LCD、12 VのACアダプター、箱を含む完全版は約70〜120ドルになります。Arduinoスターターキットをすでに持っていれば、費用はかなり下がります。
温室をスマホから監視するにはどうすればいいですか?
いちばん簡単なのは、頭脳をESP32に替えてWebサーバーを動かす方法です。同じWi-Fiネットワーク上で、スマホのブラウザから値を見たりポンプを動かしたりできます。外出先からのアクセスにはBlynk IoT、警告メッセージにはTelegramボット、長期のデータ記録にはGoogleスプレッドシートを使えます。
科学コンテストでスマート温室をどう発表すればいいですか?
動く模型を見せるだけでなく、測定できる研究の問いを立てましょう。たとえば、2つのグループの植物で数週間にわたり、センサーによる水やりが時間を決めた水やりと比べてどれだけ水を節約できるかを測ります。水の使用量、草丈、温度と湿度のグラフを表とグラフで示し、ブロック図でシステムを説明し、ライブデモ用の短い台本を用意します。
この記事を読んだ後に作れるプロジェクト
全プロジェクトを見る
DHT11で温度と湿度を測定
部屋の温度は何度で、空気は湿っているでしょうか、それとも乾燥しているでしょうか?DHT11センサーが温度と湿度を測定し、Arduinoが2行のLCDディスプレイに値を表示して、数秒ごとに更新します。センサーに異常があればエラーメッセージが表示されます。自分だけの小さな気象観測装置を作る第一歩です。

LDRで光の強さを測定
部屋の明るさを数値で見てみませんか?光に反応する小さな部品、LDRで周囲の光を測定し、Arduinoが0.5秒ごとにその値をコンピューターの画面に表示します。手で覆うと数値が変わる様子を確認できます。自動で点灯する常夜灯などのプロジェクトへの第一歩です。

ESP32とLDRモジュールで光の強さを読み取る
LDRモジュールの生のADC値と、暗い状態・明るい状態の基準値から計算した相対的な明るさの割合を、シリアルモニタでリアルタイムに確認します。この割合はルクスの測定値ではありません。モジュールの校正の仕組みを学べます。しきい値の決定に使うため、プロジェクト10の前に取り組むのが適しています。

スマート水やりステーション
旅行中も植物が枯れないよう、自動で水やりをするシステムです。土壌水分センサーで土の湿り具合を測定し、乾燥するとArduinoが給水ポンプを数秒間動かして、青いライトを点灯させます。水分が適切なときは緑のライトが点灯します。土がひどく乾燥すると、赤いライトが点滅し、ブザーが鳴って知らせます。

ESP32 DHT22温湿度計(LCDディスプレイ付き)
DHT22センサーで周囲の温度と湿度を測定し、16x2 I2C LCDにリアルタイムで表示するプロジェクトです。Adafruit DHTライブラリを使い、1本の信号線を使うプロトコルでセンサーからデータを読み取ります。IoT気象観測装置やスマートホームの自動化、温室の環境監視システムの基礎となります。ESP32のGPIO 4ピンを介してDHT22と通信します。

ESP32温度データロガー(DHT22 + DS1307 RTC + microSD + OLED)
ESP32で日時付きの温度・湿度記録システムを作ります!DHT22で1分ごとに温度と湿度を読み取り、DS1307 RTCから取得した日時とともにmicroSDカードへCSV形式で記録します。OLEDには最新の温度を大きな文字で表示し、湿度のパーセント値、日時、総記録数も表示します。SDカードの/temp_log.csvファイルはExcel、Google Sheets、Pythonのpandasで開いて分析できます。温室の監視、家や部屋の温度管理、実験室のデータ収集、食品の保管環境や養蜂箱の監視に適しています。

ESP32スマート照明(PIR + LDR + リレー + OLED)
ESP32で廊下やトイレ、食品庫の照明をスマート化します!PIRモーションセンサー、LDR光センサー、リレーモジュールを使い、「暗い場所で動きを検知すると点灯する」定番の仕組みを作ります。周囲が明るければ動きを検知しても点灯しないため、日中の無駄な電力消費を抑えられます。照明は30秒間点灯し、動きが続くとタイマーがリセットされて点灯時間が延びます。SSD1306 OLEDには大きな文字で状態(ON / DAYTIME / WAITING)とカウントダウンを表示します。
ほかの記事
BlueGraysアプリ
この回路をスマホでステップごとに作る
回路図、コード、部品リストを1つの画面で確認できます。Arduino、ESP32、Picoのプロジェクトを200件以上用意しています。

