ArduFarmBot Part 2:リモートIoT制御ステーションの構築
必要な部品と用品
| 部品 | 説明 | 数量 |
|---|---|---|
| Arduino Nano R3 | 1 | |
| Espressif ESP8266 ESP-01 | 1 | |
| DHT22 温湿度センサー | 1 | |
| KY-018 光センサーモジュール | 1 | |
| 土壌湿度センサー | 1 | |
| リレーモジュール | 2 | |
| LCD 20x4 (I2C) | 1 | |
| タクトスイッチ 12mm | 3 | |
| LED | 3 |
アプリとオンラインサービス
| サービス | 説明 |
|---|---|
| ThingSpeak API | クラウドデータストレージとして使用 |
プロジェクト概要
このチュートリアルは「ArduFarmBot: ArduinoとIoTでトマト家庭農園を制御」の続編です。
第1部ではローカル制御ステーションを作成し、トマト栽培環境の気温、相対湿度、光量、土壌湿度を取得。これらのデータに基づき、ArduFarmBotが自動で適切なタイミングと量で給水と加温を制御しました。また、手動操作でポンプとランプを操作することも可能でした。
今回の第2部では、この「手動介入」をインターネット経由でリモートからも可能にするIoTアプローチを実装します。ブロック図の通り、取得データはクラウドストレージ(ThingSpeak.com)に送信され、専用ウェブページ「リモートコントロールページ」でほぼリアルタイムに監視・表示。さらにポンプとランプの遠隔操作も可能になります。
ステップ1:部品表
- Arduino Nano - ($7.50)
- 温湿度センサー DHT22またはDHT11 - ($3.66)
- 光センサー AD-018 フォトレジスタモジュール等 - ($0.71)
- 土壌湿度センサー ×2 - ($1.99) (自作も可能)
- LCD I2C 20x4 - ($13.99)
- LED ×1 - ($0.20)
- ESP8266 ESP-01 WiFiモジュール - ($5.96)
- アクティブブザー KY-012等 - ($0.60)
- 5Vリレーモジュール ×2 - ($11.60)
- ジャンパーワイヤー - ($1.00)
- 抵抗:10kΩ、2.2kΩ、1.0kΩ、220Ω 各少々
- Arduino Nanoシールド(Funduino等) - ($7.28)
- メンブレンキーボード(4キー) - ($6.65)
- プラスチックケース
ステップ2:ハードウェアの完成
第1部で作ったローカルステーションにESP8266を追加するだけです。ブロック図に全ピン接続を記載。電圧レベルに注意:ESP8266は3.3V動作なので、NanoのTxピン(D3)を直接ESP8266のRxにつなぐことはできません。分圧抵抗でレベル変換を行います。
ESP8266の詳細については、私の以下のチュートリアルを参照:
- The ESP8266 Part 3 - Arduino LEDをリモートで点灯
- The ESP8266 Part 1 - Arduino用シリアルWiFiモジュール
- The ESP8266 Part 2 - Arduino Webサーバー
ESP8266はNanoピン2(Tx)とピン3(Rx)に接続し、SoftwareSerialライブラリを使用。ピン0,1(ハードウェアシリアル)を使う場合は、書き込み時に外す必要があります。
※ブザーを接続する場合はピンD17(=A3)に。通信エラー時に音で知らせると便利。テスト時のみ使用し、最終版ではハードウェアから外しました(コードには残っています)。
ESP8266のテスト・セットアップ用コード:FC9DBPKIT682FY7.ino
ステップ3:ESP8266接続と初期化
インターネット接続にはESP8266を使用。安価でプログラミングも容易なIoTモジュールです。まずCH_PDピンでリセット:
/***************************************************
* リセット関数
***************************************************/
void reset8266(void) {
pinMode(CH_PD, OUTPUT);
digitalWrite(CH_PD, LOW);
delay(300);
digitalWrite(CH_PD, HIGH);
Serial.print("8266 reset OK");
lcd.clear();
lcd.println("8266 reset OK ");
}
リセット後、認証情報(コード内のUSERNAME/PASSWORDを書き換え)でローカルネットワークに接続し、STAモード(CWMODE=1)で初期化:
/***************************************************
* WiFi接続
***************************************************/
void connectWiFi(void) {
sendData("AT+RST\r\n", 2000, DEBUG); // リセット
sendData("AT+CWJAP=\"USERNAME\",\"PASSWORD\"\r\n", 2000, DEBUG); // ネットワーク接続
delay(3000);
sendData("AT+CWMODE=1\r\n", 1000, DEBUG);
sendData("AT+CIFSR\r\n", 1000, DEBUG); // IPアドレス表示
lcd.clear();
lcd.print("8266 Connected");
Serial.println("8266 Connected");
}
ATコマンド送信用関数 sendData():
/***************************************************
* ATコマンド送信
***************************************************/
String sendData(String command, const int timeout, boolean debug) {
String response = "";
esp8266.print(command);
long int time = millis();
while ((time + timeout) > millis()) {
while (esp8266.available()) {
char c = esp8266.read();
response += c;
}
}
if (debug) Serial.print(response);
return response;
}
これらの関数はsetup()で呼び出されます。正しく設定されていれば、シリアルモニタに上記のようなメッセージが表示されます。
ステップ4:クラウドストレージ - ThingSpeak.com
全取得データを無料サービスのThingSpeak.comにアップロード。loop()内でセンサー読み取り(readSensors())後、updateDataThingSpeak()を呼び出します:
/***************************************************
* ThingSpeak.comへデータ送信
***************************************************/
void updateDataThingSpeak(void) {
startThingSpeakCmd();
cmd = msg;
cmd += "&field1="; cmd += tempDHT; // field1: DHT温度
cmd += "&field2="; cmd += humDHT; // field2: DHT湿度
cmd += "&field3="; cmd += lumen; // field3: 光量
cmd += "&field4="; cmd += soilMoist; // field4: 土壌湿度
cmd += "&field5="; cmd += pumpStatus; // field5: ポンプ状態
cmd += "&field6="; cmd += lampStatus; // field6: ランプ状態
cmd += "\r\n";
sendThingSpeakCmd();
}
ThingSpeakとの通信開始:
/***************************************************
* ThingSpeak.comとの通信開始
***************************************************/
void startThingSpeakCmd(void) {
cmd = "AT+CIPSTART=\"TCP\",\"";
cmd += IP;
cmd += "\",80";
esp8266.println(cmd);
delay(2000);
if (esp8266.find("Error")) {
Serial.println("ESP8266 START ERROR");
return;
}
Serial.println("Thinkspeak Comm Started");
cmd = "";
}
データ送信:
/***************************************************
* ThingSpeakチャネル更新
***************************************************/
String sendThingSpeakCmd(void) {
esp8266.print("AT+CIPSEND=");
esp8266.println(cmd.length());
if (esp8266.find(">")) {
esp8266.print(cmd);
Serial.println(""); Serial.println(""); Serial.println(cmd);
delay(500);
String messageBody = "";
while (esp8266.available()) {
String line = esp8266.readStringUntil('\n');
if (line.length() == 1) {
messageBody = esp8266.readStringUntil('\n');
Serial.print("Message received: "); Serial.println(messageBody);
}
}
return messageBody;
} else {
esp8266.println("AT+CIPCLOSE");
Serial.println("ESP8266 CIPSEND ERROR: RESENDING");
error = 1;
return "error";
}
}
これらの関数はMichalis Vasilakis氏の詳細なチュートリアルをベースにしています。詳細はArduino IOT: Temperature and Humidity (with ESP8266 WiFi)を参照。
ステップ5:Webからアクチュエーターを制御
データをクラウドにアップロードして監視できるようになりましたが、取得データに基づいてリモートからポンプやランプをON/OFFしたい場合もあります。そのため、ThingSpeakチャネルに制御用フィールドを追加:
- Field 7: データ=1 → ポンプON、データ=0 → ポンプOFF
- Field 8: データ=1 → ランプON、データ=0 → ランプOFF
ThingSpeakでこれらのフィールドを設定するには、プラグインを書くか、外部Webサイトから操作します(ここでは後者を採用)。以下のコマンドで書き込み:
api.thingspeak.com/update?key=YOUR_WRITE_KEY&field7=1
ブラウザで実行すると、該当フィールドが更新されます。返り値はシリアル番号です。
次に、この「命令」をローカルのArduFarmBotで読み取ります:
api.thingspeak.com/channels/CHANNEL_ID/fields/7/last
ブラウザでテストすると、そのフィールドの最新値が表示されます。
フィールド7の最新データを読み取る関数:
/***************************************************
* ThingSpeak field7の最新データ読み取り
***************************************************/
int readLastDataField7() {
startThingSpeakCmd();
cmd = msgReadLastDataField7;
cmd += "\r\n";
String messageDown = sendThingSpeakCmd();
Serial.print("Command received: "); Serial.println(messageDown[7]);
int command = messageDown[7] - 48; // ASCII '0'=48
return command;
}
フィールド8用も同様に作成。取得した命令でアクチュエーターを制御:
/***************************************************
* ThingSpeakから命令を受信してアクチュエーター制御
***************************************************/
void receiveCommands() {
field7Data = readLastDataField7();
if (field7Data == 1) { digitalWrite(PUMP_PIN, HIGH); pumpStatus = 1; showDataLCD(); }
if (field7Data == 0) { digitalWrite(PUMP_PIN, LOW); pumpStatus = 0; showDataLCD(); }
delay(500);
field8Data = readLastDataField8();
if (field8Data == 1) { digitalWrite(LAMP_PIN, HIGH); lampStatus = 1; showDataLCD(); }
if (field8Data == 0) { digitalWrite(LAMP_PIN, LOW); lampStatus = 0; showDataLCD(); }
delay(500);
}
これでブラウザからポンプとランプをリモート操作可能。シリアルモニタに受信コマンドが表示されます。
ローカル操作とリモート操作の「調整」も重要。readLocalCmd() 関数でローカルボタン操作時にThingSpeakのField 7,8も同期更新するよう修正:
field7Data = pumpStatus;
field8Data = lampStatus;
これでWeb命令とローカル操作が同期。実際にアクチュエーターを動かす aplyCmd() 関数:
/***************************************************
* 命令を受けてアクチュエーター駆動
***************************************************/
void aplyCmd() {
if (field7Data == 1) digitalWrite(PUMP_PIN, HIGH);
if (field7Data == 0) digitalWrite(PUMP_PIN, LOW);
if (field8Data == 1) digitalWrite(LAMP_PIN, HIGH);
if (field8Data == 0) digitalWrite(LAMP_PIN, LOW);
}
テストすると、ローカル/Webどちらの手動操作も、自動制御関数 autoControlPlantation() に上書きされてしまいます。「最終決定権」を誰にするか設計が必要です。ここでは以下の優先順位としました:
- 毎ループ:ローカルボタン押下をチェック
- 約毎分:ThingSpeakをポーリングしてリモート命令確認
- 約毎10分:センサー読み取り、ThingSpeak更新、自動制御実行(これが最優先で維持される)
タイマーを2つ使用:
long sampleTimingSeconds = 75; // センサー読み取り間隔(秒)
long poolingRemoteCmdSeconds = 20; // リモート命令ポーリング間隔(秒)
long startTiming = 0, elapsedTime = 0;
long startRemoteCmdTiming = 0, elapsedRemoteCmdTime = 0;
int reverseElapsedTimeSeconds = 0;
書き換えた loop():
void loop() {
elapsedRemoteCmdTime = millis() - startRemoteCmdTiming;
elapsedTime = millis() - startTiming;
reverseElapsedTimeSeconds = round(sampleTimingSeconds - elapsedTime/1000);
readLocalCmd(); // ローカルボタン読み取り
showDataLCD(); // LCD表示
if (elapsedRemoteCmdTime > (poolingRemoteCmdSeconds * 1000)) {
receiveCommands(); // リモート命令受信
updateDataThingSpeak(); // 状態アップロード
startRemoteCmdTiming = millis();
}
if (elapsedTime > (sampleTimingSeconds * 1000)) {
readSensors(); // センサー読み取り
autoControlPlantation(); // 自動制御
updateDataThingSpeak(); // データアップロード
startTiming = millis();
}
}
ステップ6:専用Webページの実装
この段階でArduFarmBotはWebから制御可能ですが、ブラウザでコマンド打ち込みは「エレガント」ではありません。完全なIoTソリューションには、データ表示とアクチュエーター操作ボタンを備えた専用Webページが必要です。
無料ホスティングのByethostを使用。Coursera/ミシガン大学の「Webデザインと作成」スペシャリゼーション(HTML5, CSS3, JavaScript)で学んだ知識を活用しました。
Webページ開発の詳細は本筋ではないため割愛しますが、HTML/CSS/JavaScriptのソースコードを同梱しています。興味があればコメント欄で議論しましょう。
重要:このページはArduFarmBotローカルステーションと直接通信しません。ThingSpeakチャネルと以下のようにやり取りします:
- フィールド1〜4:センサーデータ読み取り
- フィールド5,6:アクチュエーター状態読み取り
- フィールド7,8:制御命令書き込み
- Yahoo天気サービスからローカル気象データ取得
項目4は必須ではありませんが、ローカルのトマト栽培とは独立にリモート判断する際の参考データになります。別のThingSpeakチャネルに保存してArduinoにダウンロードし、ローカルLCDに天気表示するのも面白いです(別プロジェクトで実装済み)。
Webページソース:FQOK9ENIT653YX5.zip
ステップ7:センサー・アクチュエーターマトリクスアプローチ
第1部ではアクチュエーターの動作条件をシンプルに定義しましたが、複雑な状況組み合わせには対応できません。「センサー・アクチュエーターマトリクス」で体系化します。
各センサーの条件とアクチュエーター出力をマトリクスで定義(同梱Excelファイル参照)。フィルター機能付きテーブルと、複数条件選択でアクチュエーター動作を確認できるバージョンの2シート構成。
これをコードに落とし込み、18行×10列の配列で条件を管理:
// +---SOIL----+-LIGHT-+---TEMP---+---ACTUAT----+
// SL SM SH LL LH TL TM TH Pump Lamp
boolean SDf[18][10] = {
{1,0,0, 0,1, 0,0,1, 1,0},
{1,0,0, 0,1, 0,1,0, 1,0},
{1,0,0, 0,1, 1,0,0, 1,1},
{1,0,0, 1,0, 0,0,1, 1,0},
{1,0,0, 1,0, 0,1,0, 1,0},
{1,0,0, 1,0, 1,0,0, 0,1},
{0,1,0, 0,1, 0,0,1, 0,0},
{0,1,0, 0,1, 0,1,0, 0,0},
{0,1,0, 0,1, 1,0,0, 0,1},
{0,1,0, 1,0, 0,0,1, 0,0},
{0,1,0, 1,0, 0,1,0, 0,1},
{0,1,0, 1,0, 1,0,0, 0,1},
{0,0,1, 0,1, 0,0,1, 0,0},
{0,0,1, 0,1, 0,1,0, 0,0},
{0,0,1, 0,1, 1,0,0, 0,1},
{0,0,1, 1,0, 0,0,1, 0,0},
{0,0,1, 1,0, 0,1,0, 0,1},
{0,0,1, 1,0, 1,0,0, 0,1},
};
defSensorStatus() 関数で各行の最初の8列条件が全てTRUEなら、最後の2列(ポンプ、ランプ)を実行。内部でセンサー状態配列 boolean snsSts[8] も作成し、ログ記録にも使用。
Excelファイル:F2HWXBYITA8WIN1.xlsx
ステップ8:コード最適化
開発過程で仕様変更を行いました:
ディスプレイ
LCDはデフォルトOFF。センサー読み取りボタンをトグルとして、任意のタイミングでON/OFF可能。表示ON/OFF時はセンサー値を更新して表示しますが、通常制御には使用しません。
初期セットアップ
起動/リセット時に「初期設定」画面を表示。センサーボタン押下でプログラム開始。表示項目:
- 低温閾値(例:12℃)
- 乾燥土壌閾値(例:30%)
- 湿潤土壌閾値(例:60%)
- 暗さ閾値(例:40%)
- ポンプON時間(例:10秒)
- スキャン間隔(例:600秒)
- ソフトウェアバージョン(例:4.1)
ログ記録
監査用にセンサー読み取りとアクチュエーションのログをEEPROMに記録。各読み取りサイクルで storeDataLogEEPROM() を実行:
/*************************************************** * Arduino EEPROMへログデータ保存 ***************************************************/ void storeDataLogEEPROM(void) { for (int i = 0; i < 8; i++) { logData = logData + (snsSts[i] << 1); } EEPROM.write(memoAddr, logData); memoAddr++; logData = 0; logData = logData + pumpStatus; logData = logData << 1; logData = logData + lampStatus; EEPROM.write(memoAddr, logData); EEPROM.write(0, memoAddr + 1); logData = 0; if ((memoAddr + 1) == 1023) memoAddr = 1; else memoAddr++; }
snsSts[]のビット列+ポンプ/ランプ状態をEEPROMに格納。シリアルモニタのログ表示例は写真参照。コードは複数ファイルに分割。第2部で新規追加:
- communication.ino - ThingSpeak/ESP8266専用関数
- stationCredentials.h - ThingSpeakチャネルIDとライトキー
コードサイズ増大に対応し、定数データをフラッシュメモリ(PROGMEM)に配置してSRAMを節約:
// #define DHTPIN 5 の代わりに const PROGMEM byte DHTPIN = 5;
avr/pgmspace.hをインクルード。デバッグ用Serial.print()はコメントアウトまたは削除(ログ表示コード等もコメントアウト済み)。完全なArduFarmBotコード一式:
- FTUT5VIIT67U8ME.ino
- FWMIPSSIT67U8MG.ino
- FJPGZNKIT67U8MK.ino
- FQH3X9VIT67U8NA.ino
- F15MY4YIT67U8NB.ino
- FVU64X1IT67U8NC.h
- FZ057E3IT67U8P5.h
ステップ9:MJRovaiホームファーム
ArduFarmBotで制御した最初のトマト栽培の連続写真:
2回目の栽培:種まきから約45日で優良苗6株を選抜・移植するまで:
ステップ10:結論
今回はここまで!
このプロジェクトが、エレクトロニクス、IoT、ロボティクスの魅力的な世界への道しるべになれば幸いです。
近いうちに第3部(最終回)として、オーガニックトマトソースパスタのレシピを公開予定です!
写真にある通り、マウリシオの栽培にも生命の兆しが。最後に、新刊『ArduFarmBot, the book!』をご紹介:
ArduFarmBot, the Book!
Amazon.comでも購入可能。電子コントローラー「ArduFarmBot」を題材に、ハードウェアとソフトウェアの両面から学べる実践書:
- LCD/OLEDディスプレイ
- LEDとボタン
- リレーによるポンプ/ランプ制御
- センサー:DHT22(気温・湿度)、DS18B20(土壌温度)、YL69(土壌湿度)、LDR(光量)
全工程を説明文、ブロック図、高解像度カラー写真、Fritzing回路図、GitHub格納コード、YouTube動画で詳細に文書化。
第1部:Arduino Nanoベースのローカル版ArduFarmBot(手動/自動制御)
第2部:ESP8266-01でWi-Fi接続、ThingSpeak.com連携、Webページ作成によるリモート操作
第3部:NodeMCU ESP8266-12E導入でArduino NanoとESP8266-01を置き換え、Blynkプラットフォームも活用
書籍をダウンロードしてレビューを書いてください。コメント欄でご意見・ご提案・ご批判をお待ちしています!
その他のプロジェクトはブログ MJRoBot.org で公開中。
南半球からサルードス!
次のプロジェクトでお会いしましょう!
ありがとう
Marcelo
コードスニペット
主要関数をスニペットとして掲載(完全版は上記ファイル参照):
- Code snippet #1:
reset8266() - Code snippet #2:
connectWiFi() - Code snippet #3:
sendData() - Code snippet #4:
updateDataThingSpeak() - Code snippet #5:
startThingSpeakCmd() - Code snippet #6:
sendThingSpeakCmd() - Code snippet #9:
readLastDataField7() - Code snippet #10:
receiveCommands() - Code snippet #11:
aplyCmd() - Code snippet #12: タイマー変数定義
- Code snippet #13:
loop()メインループ - Code snippet #14: センサー・アクチュエーターマトリクス配列
- Code snippet #15:
storeDataLogEEPROM()
GitHubリポジトリ:https://github.com/Mjrovai/ArduFarmBot
回路図
製造プロセス
- Arduino制御の6連ショット・ドリンクミキシングステーション – 自作カクテル自動製造機
- スマートホーム連携向けマルチポジション温度センサーシステム
- MotionSense: ArduinoとESP8266で作るスマート侵入検知システム
- 3Dプリンター火災予防:スマートセンサーセーフティシステム
- Arduino、Yaler、IFTTTでWeb対応IoTゲージを構築する
- Arduinoで作る斥力式磁気浮上装置: シンプル回路とDIYガイド
- 高度なリモートコントロールグリッパーロボット: 精密ハンドリング向け
- 高性能 Arduino 気象観測気球データロガーの製作ガイド
- 10ドルで作る携帯型Arduino気象観測ステーション – Nano、DHT22、BMP180、LCDで簡単DIY
- CoroFenceサーマルディテクター:Grid-EYE & PIRセンサー搭載IoT体温監視
- IoT実装における新興トレンド:インサイトと戦略