ガイド · 読了まで27分

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版、学校や科学コンテストでの発表のコツ、よくある失敗を紹介します。

DHT22(AM2302)
DHT22(AM2302)
LDR(フォトレジスタ)
LDR(フォトレジスタ)
I2C LCD(16x2)
I2C LCD(16x2)
LM2596降圧モジュール
LM2596降圧モジュール
Arduino Uno
Arduino Uno

スマート温室はどのように動く?

どのスマート温室も、測る、判断する、動かす、表示するという同じ4段階のループで動いています。センサーが環境を測ります。中はどれくらい暖かいか、空気はどれくらい湿っているか、土は乾いたか、光は足りているか、といったことです。Arduinoはこれらの値を、自分で決めたルールと比較します。「温度が30 °Cを超えたらファンを回す」「土壌水分が35%を下回ったら水をやる」といったルールです。アクチュエーターがその判断を実行します。リレーがファン、ヒーター、ポンプをオン・オフし、MOSFETがLEDテープの明るさを調整します。最後に、LCDとスマホに温室の現在の状態が表示されます。実際の商業用温室の環境制御コンピューターもまさにこのループで動いていて、センサーの数が多く、モーターが大きいだけです。

何を測る?センサーアクチュエータールール(例)
気温DHT22ファンとヒーター30 °Cでファンが回り、16 °Cでヒーターがオン
相対湿度DHT22ファン85%でファンが回り、75%で止まる
土壌水分静電容量式の土壌水分センサー小型の水中ポンプ35%を下回ると3秒ずつ区切って水やり
光LDRLEDテープ(MOSFET)暗くなるとLEDが点灯し、最大4時間で消灯
タンクの水フロート式水位スイッチポンプの動作禁止タンクが空ならポンプは絶対に動かない

植物に必要なものは? 目標の温度、湿度、光

ルールを書く前に、何を目標にするかを知っておく必要があります。ほとんどの植物は昼と夜で異なる温度を好み、夜に少し涼しくなると植物は休めます。湿度が長時間85%を超えると葉に水滴がつき、カビやべと病などの病気が一気に広がります。だからこそ温室のファンは、冷やすためだけでなく湿気を逃がすためにも回ります。光については、ほとんどの野菜の苗は1日14〜16時間を好みますが、少なくとも6〜8時間の暗い時間も必要です。LEDを一晩中つけたままにすると、植物にとってはよいことより害のほうが大きくなります。下の値はよい出発点の目安なので、育てる植物に合わせて調整してください。

植物昼夜相対湿度メモ
トマト21〜27 °C15〜18 °C60〜75%32 °Cを超えると花が落ちることがある
ピーマン21〜29 °C16〜20 °C60〜75%トマトより寒さに弱い
レタスとホウレンソウ15〜20 °C10〜15 °C60〜70%暑さと長い日照で早くとう立ちする
イチゴ18〜24 °C10〜13 °C60〜75%湿度が高いと灰色かび病のリスクが高まる
苗と発芽20〜25 °C18〜20 °C70〜85%気温より地温のほうが重要
発表の日に教室が22 °Cなら、30 °Cで回るように設定したファンはいつまでも回りません。発表用には、しきい値を室温より2〜3度高く設定しておきましょう。そうすれば、手のひらでDHT22を温めたとき、審査員は数秒でファンが回り始めるのを見られます。実際に使うときは、表の値に戻してください。

模型の温室を作る

いちばん簡単で安い箱はふた付きの透明なプラスチック収納ボックスです。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 DevKit1温室の頭脳中程度
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 m1補光安い
I2C接続の16x2 LCD1状態の表示安い〜中程度
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光センサーの記事を使ってください。

LDRにLEDの光が見えると、システムは自分でオン・オフを繰り返すループに陥ります。暗くなるとLEDが点灯し、LDRは明るいと判断して消灯し、また暗さを検知します。これを防ぐには、LDRをLEDから離して温室の外に置き、コードでは1つのしきい値ではなくヒステリシスを使います。静電容量式の土壌センサーの上部にある電子回路も防水ではありません。センサーに印のある線より上まで埋めないようにし、基板の縁をホットボンドか透明なマニキュアで封止してください。

アクチュエーター:リレーか、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ロジックレベルMOSFETPWMによる明るさの制御と静かな動作
小型の水中ポンプ
3〜6 Vの小型水中ポンプ:水タンクの中に沈め、約100〜200 mAを消費し、ホースを通して鉢へ水を送ります。空運転は絶対にさせないでください。

小型の水中ポンプは、ホースの高さにもよりますが毎分約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のラインにヒーターと直列に温度ヒューズを入れて、ヒーターを取り付けた板に接触させます。そして、目を離す前に何時間もシステムをテストしてください。ヒューズは、ヒーターの制限温度より少し高い温度で切れるものを選びます(たとえば80 °CのPTCなら92 °C)。定格が低すぎると、通常の動作中に理由もなく切れてしまいます。育苗用のヒートマットや大きなヒーターは多くの場合商用電源で動き、商用電源は命にかかわります。模型の温室では商用電源を使わないでください。12 VのPTCヒーターで十分です。どうしても商用電源の機器が必要なら、配線は電気工事士か知識のある大人が行い、リレーボードは閉じた絶縁ケースに入れなければなりません。安全な配線のルールはリレーモジュールとは?の記事にあります。

電源の計画: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 V2 A(冷えているときの最初の瞬間はそれ以上)
LEDテープ(1 m)12 V1〜1.2 A
80 mmのファン2台12 V0.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 V0.2〜0.25 A
小型の水中ポンプ5 V0.1〜0.25 A(起動時はそれ以上)
I2C LCD(バックライト点灯)5 V20〜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 UnoESP32Raspberry Pi Pico
DHT22のDATAD2GPIO 4GP15
フロートスイッチ(もう一方の線はGNDへ)D3GPIO 13GP14
リレーIN3:ポンプD4GPIO 25GP18
リレーIN1:ファンD7GPIO 26GP16
リレーIN2:ヒーターD8GPIO 27GP17
MOSFETのゲート(220 Ω経由):LEDD9GPIO 18GP19
土壌水分センサーのAOUTA0GPIO 34GP26
LDR分圧回路の中点A1GPIO 35(3V3からの分圧)GP27(3V3からの分圧)
I2C LCDのSDA / SCLA4 / A5GPIO 21 / 22GP4 / GP5
センサーの電源5V3V33V3
ESP32とPicoのピンは3.3 Vで動作し、5 Vトレラントではありません。これらのボードでは、DHT22、土壌水分センサー、LDRの分圧回路を3V3ピンから給電し、アナログピンに3.3 Vを超える電圧がかからないようにします。アクティブLOWの5 Vリレーモジュールは、3.3 VのHIGH信号では完全にオフにならないことがあります。リレーがオンのままになる場合は、JD-VCC付きのモジュールでVCCを3.3 Vから、JD-VCCを5 Vから給電するか、3.3 Vでトリガーできるモジュールを購入してください。LEDテープには、3.3 Vのゲート電圧で完全にオンになるMOSFET(たとえばIRLB8721)を選びます。

しきい値とヒステリシス:リレーがカチカチと切り替わり続けるのはなぜ?

1つのしきい値でファンを制御する場合を考えてみましょう。「温度が30 °Cを超えたら回し、そうでなければ止める」。温度が29.9〜30.1 °Cの間で揺れると(センサーのノイズでよく起こります)、リレーは読み取りのたびにオン・オフします。リレーの接点もファンのモーターも消耗し、ヒーターの場合はオンになるたびに配線とACアダプターに無駄な負担がかかります。解決策はヒステリシスです。オンとオフに別々のしきい値を使います。ファンは30 °Cで回り始めますが、止まるのは温度が27 °Cまで下がったときだけです。その間の3度の不感帯では、システムは直前の状態を保ちます。給湯器の室温サーモスタットや冷蔵庫も同じ原理で動いています。

制御オンになるオフになる不感帯
ファン(温度)≥ 30 °C≤ 27 °C3 °C
ファン(湿度)≥ 85%≤ 75%10%
ヒーター≤ 16 °C≥ 19 °C3 °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でグラフを描けます。

ステップごとの組み立てとテスト

  1. 箱を準備します。ファン、吸気口、ホース、ケーブル用の穴を切り抜き、電子部品の箱を温室の外に置きます。
  2. 電源のラインを用意します。12 VのACアダプターをネジ端子につなぎ、テスターを見ながらLM2596を5.0 Vに合わせ、GNDをすべてつなぎます。
  3. I2C LCDをつなぎ、何か文字を表示させます。何も表示されない場合は、I2C LCDディスプレイの記事にあるスキャナーのスケッチとコントラスト調整用の半固定抵抗を試してください。
  4. センサーを1つずつ追加し、シリアルモニタでテストします。DHT22の読み取り値、土壌センサーの空気中と水中での生の値、LDRの暗いところと日なたでの値を書き留めます。
  5. まず負荷をつながずに、LEDを見てカチッという音を聞きながらリレーをテストします。次にファン、ポンプ(タンクを満たした状態で)、最後にヒーターをつなぎます。
  6. LEDテープをMOSFET経由でつなぎ、analogWrite()の値を変えて明るさが変わるのを確認します。
  7. メインのコードを書き込み、キャリブレーションの値を定数に入力して、見守りながら数時間システムを動かします。水やりの間違い、カチカチ鳴り続けるリレー、熱くなりすぎる部品はないでしょうか?
すべてを一度に配線して、一気に動かそうとしないでください。6つのモジュールがうまく連携しないとき、故障箇所を探すのに何時間もかかることがあります。1つずつテストしたモジュールは、組み合わせてもたいてい一度で動きます。

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の値を入れ替えてください。

C++ (Arduino IDE)
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でスマホから監視と水やり

ESP32開発ボード
ESP32:Wi-Fiでスマホから温室を監視でき、インターネットから取得した時刻を使って実際の時計どおりにLEDを動かせます。

UnoにはWi-Fiがありません。温室の状態をスマホで見たり、離れた場所から水やりをしたりしたいなら、頭脳をESP32に替えましょう。下のコードは同じ制御ロジックをESP32に移し、小さなWebサーバーを起動します。スマホを同じWi-Fiネットワークにつなぎ、シリアルモニタに表示されたIPアドレスをブラウザで開くと、温度、湿度、土壌水分、明るさ、タンクの状態、各機器のオン・オフが表示されます。ページは10秒ごとに自動で更新されます。Water now(今すぐ水やり)ボタンを押すと、タンクに水があればポンプが3秒間1回だけ動きます。起動時にESP32はインターネット(NTP)から時刻を取得するので、LEDは06:00〜20:00の間で、しかも外が暗いときだけ点灯し、植物は夜に休めます。

C++ (Arduino IDE)
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です。

学校にWi-Fiがない場合は、WiFi.begin()の行とその後の待機ループをWiFi.softAP("BlueGrays-Sera", "12345678")に置き換えれば、ESP32が自分のネットワークを作ります。スマホをそのネットワークにつなぎ、192.168.4.1を開きます。この場合はインターネットがないので時刻を取得できず、LEDは予備のルールを使います。
方法外出先からのアクセス難易度向いている用途
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サーバーの記事の方法を加えれば、スマホから値を確認できます。

MicroPython
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の設定を紙に書いておきます。
  • 審査員の質問に備える:「センサーが故障したらどうなりますか?」「停電したら植物はどうなりますか?」「本物の温室に規模を広げるにはどうしますか?」に短く答えられるようにしておきます。コードにある安全な状態とあふれ防止は、こうした質問へのいちばんの答えです。
テーマ別のコンテストに出るなら、このプロジェクトに最も自然に合うのは農業・食品技術の分野です(TÜBİTAK 4006では農業・畜産技術のテーマ)。ほかのアイデア、費用の表、審査基準についてはArduinoの科学コンテスト向けプロジェクトアイデアの記事を参照してください。

よくある失敗と対処法

症状考えられる原因対処
ポンプが動くと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で温度と湿度を測定
#55中級

DHT11で温度と湿度を測定

部屋の温度は何度で、空気は湿っているでしょうか、それとも乾燥しているでしょうか?DHT11センサーが温度と湿度を測定し、Arduinoが2行のLCDディスプレイに値を表示して、数秒ごとに更新します。センサーに異常があればエラーメッセージが表示されます。自分だけの小さな気象観測装置を作る第一歩です。

4.7(47)
73
LDRで光の強さを測定
#11初級

LDRで光の強さを測定

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

4.6(81)
139
ESP32とLDRモジュールで光の強さを読み取る
#113初級

ESP32とLDRモジュールで光の強さを読み取る

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

4.6(36)
55
スマート水やりステーション
#83上級
プレミアム

スマート水やりステーション

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

5.0(42)
57
ESP32 DHT22温湿度計(LCDディスプレイ付き)
#117中級

ESP32 DHT22温湿度計(LCDディスプレイ付き)

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

4.8(38)
56
ESP32温度データロガー(DHT22 + DS1307 RTC + microSD + OLED)
#144上級
プレミアム

ESP32温度データロガー(DHT22 + DS1307 RTC + microSD + OLED)

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

4.7(7)
12
ESP32スマート照明(PIR + LDR + リレー + OLED)
#141上級
プレミアム

ESP32スマート照明(PIR + LDR + リレー + OLED)

ESP32で廊下やトイレ、食品庫の照明をスマート化します!PIRモーションセンサー、LDR光センサー、リレーモジュールを使い、「暗い場所で動きを検知すると点灯する」定番の仕組みを作ります。周囲が明るければ動きを検知しても点灯しないため、日中の無駄な電力消費を抑えられます。照明は30秒間点灯し、動きが続くとタイマーがリセットされて点灯時間が延びます。SSD1306 OLEDには大きな文字で状態(ON / DAYTIME / WAITING)とカウントダウンを表示します。

4.3(6)
11