はじめに
こんにちは。IoT コンサルタントの宮本です。
先日、とあるイベントでお花の種を頂きました。私は、お花は鑑賞するのは好きなのですが、育てた経験はほとんどありません。ネットで軽く、お花の育て方を調べてから、種を蒔き、育ててみました。無事に芽は出てきたのですが、「水やりって、どのようなタイミングでやれば良いの ?」と疑問を持ちました。また、水やりを忘れてしまう日もあって、これは良くない、何とかしようと思い、「自動水やりシステム」を開発することにしました !
まずは、どんな機能を実現するのか、を明確にします。理想的には、下記のようなシステムを作りたい、と考えていました。
土壌の水分量に基づいて、水を供給 / 停止する
水分量に加えて、気温・湿度もインプット情報に加える
花の育成状況をカメラで撮影し、日々の状態を記録する
更に、機械学習を使って、撮像より、植物の成長度合いを解析し、最適な水やり周期にフィードバックする
全てを一度に作るのはハードルが高そうなので、今回は開発のハードルを下げて、1 番目の「土壌の水分量に基づいて、水を供給/停止する」のみを実現することにしました。
ご注意
本記事で紹介する AWS サービスを起動する際には、料金がかかります。builders.flash メールメンバー特典の、クラウドレシピ向けクレジットコードプレゼントの入手をお勧めします。
builders.flash メールメンバー登録
builders.flash メールメンバー登録で、毎月の最新アップデート情報とともに、AWS を無料でお試しいただけるクレジットコードを受け取ることができます。
1. 開発の方針
前述のようなシンプルな仕様であれば、システムの作り方には様々な方法が考えられるかと思います。今回は、下記の方針を立て、開発することにしました。
2. 設計
2-1. システムの全体アーキテクチャ
今回開発するシステムの機能は、「土壌の水分量に基づいて、水を供給/停止する」だけであり、とてもシンプルです。この機能を実現するためのシステム構成を前述の開発方針を考慮して設計していきます。
デバイス側にはマイコンを使用するため、デバイスとクラウドとの通信には、比較的に処理負荷の小さい軽量プロトコルである MQTT を使ってクラウド側と通信する
AWS で MQTT を使う場合には、MQTT ブローカーの機能を持つ AWS IoT Core を使用する
クラウド側のロジックの実現には、ノーコードで開発可能な AWS IoT Events を活用する
アーキテクチャ図
2-2. デバイス側のハードウェア設計
デバイス側のハードウェアは、以下のものを使用します。
制御用マイコンデバイス : 今回、比較的に広く利用されている ESP32 マイコンを活用することにしました。手元に M5StickC Plus があったので、それを使用します。
水分量計 : 水分量系には、土壌の水分量の計測には、5 個入りで 1,000 円程度 ( 2024 年 2 月現在) と比較的に安価な DiyStudio 土壌湿度計 を使用します。

水を供給するポンプ
水を供給するポンプ : CQ出版社 Interface 2023年3月号 に掲載されている「土壌水分センサのアナログ電圧出力を A-D コンバータで読み取って表示する測定値をアナログ電圧や電流で出力するセンサ」の記事を参考に、WayinTop ミニ 小型ポンプ を選定しました。

2-3. システムの機能仕様
システムの機能仕様は、以下の通りです。
デバイスは、水分量センサー情報を周期的に AWS IoT Core に Publish する
クラウド側では、AWS IoT Core がデバイスから受信した水分量センサー情報を AWS IoT Events に送信する
AWS IoT Events では、受信した水分量センサー情報に基づいて、土壌の乾燥状態を判断しポンプの On/Off をデバイスに指示する
デバイスへのポンプの On/Off の指示は、Device Shadow を介して送信する
デバイスは、Device Shadow の値を読み取り、その指示に基づいて、ポンプを On/Off する
3. AWS IoT Core の設定
4. AWS IoT Events の設定
水分量センサーの値に応じてポンプを On/Off するロジックは、 AWS IoT Events を使って実装します。
IoT Events では、ブラウザベースの GUI である AWS IoT Events コンソールを使って、ノーコードでロジックを実装することが可能です。IoT Events を使うと、下図のような探知器モデルを作成できます。
探知器モデルは、イベントの検知と対応アクションを定義する仕組みです。デバイスやシステムの異常をリアルタイムで監視し、特定の条件が満たされた際にアラートや自動処理をトリガーする、といった使い方ができます。
探知器モデルには、状態、移行イベント、アクションの 3 つの主要要素が含まれます。状態はデバイスの状態を表し、移行イベントは状態間の遷移を定義し、アクションは特定の状態で実行される処理を指定します。探知器モデルの設定方法を記載します。

4-1. AWS IoT Events の⼊⼒の定義
デバイスが取得した水分量計のデータは、下記のようなフォーマットで AWS IoT Core に送信します。
{
"moisture": 2522
}
AWS IoT Events の入力を作成
この JSON を input.json という名称でファイルに保存します。下記の手順で、AWS IoT Events の入力を作成します。
「Create input」 ボタンを選択します。
入力名 を moisture_input と入力します。
JSON ファイルのアップロード にて JSON ファイルを選択します。ここで、先ほど作成した input.json ファイルを選択します。
「作成」ボタンを押します。
4-2. AWS IoT Events が使用する IAM Role の作成
続いて、探知器モデル が使用する IAM Role を作成します。
この探知器モデルでは、デバイスから受信した水分量計の値に応じて、Device Shadow を介して、ポンプの On/Off を指示します。そのために、探知器モデルで Device Shadow のトピックに Publish できるように、以下の IAM Policy を持つ IAM Role を作成します。
IAM Role を作成
ここではロール名を IoTEvents_watering_machine_role とします。本例では、東京リージョン (ap-northeast-1) を使用していますが、他のリージョンを使用する場合には、変更してください。また、アカウント ID は、ご自身の AWS アカウントの ID に置き換えます。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iot:UpdateThingShadow"
],
"Resource": "arn:aws:iot:your-region:{アカウントID}:thing/m5stickc-plus"
}
]
}
信頼ポリシーを設定
IoT Events がこのロールを引き受けられるように、信頼ポリシー には下記を設定します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "iotevents.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
4-3. AWS IoT Events 探知器の定義
探知器モデル で アクション > 探知器モデルのインポートを選択し、以下のモデル watering_machine をインポートします。アカウント ID は、ご自身の AWS アカウントの ID に置き換えます。
watering_machine をインポート
アカウント ID は、ご自身の AWS アカウントの ID に置き換えます。
{
"detectorModelDefinition": {
"states": [
{
"stateName": "PumpOff",
"onInput": {
"events": [
{
"eventName": "checkMoisture",
"condition": "$input.moisture_input.moisture > 2000",
"actions": [
{
"setVariable": {
"variableName": "dryCounter",
"value": "$variable.dryCounter + 1"
}
}
]
}
],
"transitionEvents": [
{
"eventName": "to_pump_on",
"condition": "$variable.dryCounter > 10",
"actions": [],
"nextState": "PumpOn"
}
]
},
"onEnter": {
"events": [
{
"eventName": "pump_off",
"condition": "true",
"actions": [
{
"iotTopicPublish": {
"mqttTopic": "$aws/things/m5stickc-plus/shadow/update",
"payload": {
"contentExpression": "'{\"state\": {\"desired\": \n{\"pumpOn\": 0 }}}'",
"type": "JSON"
}
}
},
{
"setVariable": {
"variableName": "dryCounter",
"value": "0"
}
}
]
}
]
},
"onExit": {
"events": []
}
},
{
"stateName": "PumpOn",
"onInput": {
"events": [
{
"eventName": "count_up",
"condition": "$input.moisture_input.moisture < 1800",
"actions": [
{
"setVariable": {
"variableName": "wetCounter",
"value": "$variable.wetCounter + 1"
}
}
]
}
],
"transitionEvents": [
{
"eventName": "to_pump_off",
"condition": "(timeout(\"pumpOffTimer\")) || ($variable.wetCounter > 0)",
"actions": [
{
"clearTimer": {
"timerName": "pumpOffTimer"
}
}
],
"nextState": "Wait"
}
]
},
"onEnter": {
"events": [
{
"eventName": "pump_on",
"condition": "true",
"actions": [
{
"iotTopicPublish": {
"mqttTopic": "$aws/things/m5stickc-plus/shadow/update",
"payload": {
"contentExpression": "'{\"state\": {\"desired\": {\"pumpOn\": 1 }}}'",
"type": "JSON"
}
}
},
{
"setTimer": {
"timerName": "pumpOffTimer",
"seconds": 60,
"durationExpression": null
}
},
{
"setVariable": {
"variableName": "wetCounter",
"value": "0"
}
}
]
}
]
},
"onExit": {
"events": [
{
"eventName": "pump_off",
"condition": "true",
"actions": [
{
"iotTopicPublish": {
"mqttTopic": "$aws/things/m5stickc-plus/shadow/update",
"payload": {
"contentExpression": "'{\"state\": {\"desired\": {\"pumpOn\": 0 }}}'\n",
"type": "JSON"
}
}
}
]
}
]
}
},
{
"stateName": "Wait",
"onInput": {
"events": [],
"transitionEvents": [
{
"eventName": "pumpOn_wait_timeout",
"condition": "timeout(\"pumpOnWaitTimer\")",
"actions": [
{
"clearTimer": {
"timerName": "pumpOnWaitTimer"
}
}
],
"nextState": "PumpOff"
}
]
},
"onEnter": {
"events": [
{
"eventName": "StartWaitTimer",
"condition": "true",
"actions": [
{
"setTimer": {
"timerName": "pumpOnWaitTimer",
"seconds": 600,
"durationExpression": null
}
},
{
"setVariable": {
"variableName": "dummy",
"value": "0"
}
}
]
}
]
},
"onExit": {
"events": []
}
}
],
"initialStateName": "PumpOff"
},
"detectorModelDescription": "水分量計の値に応じてポンプを On/Off する探知機モデル",
"detectorModelName": "watering_machine",
"evaluationMethod": "SERIAL",
"key": null,
"roleArn": "arn:aws:iam::{アカウントID}:role/service-role/IoTEvents_watering_machine_role"
}
モデルの状態と遷移条件
このモデルの状態と遷移条件は以下の表のようになっています。各状態を表す丸いノードと矢印をクリックするとそれぞれのアクションと遷移の条件式が表示されるので、確認してみてください。
状態 |
説明 |
PumpOff |
ポンプ Off の状態。本状態に遷移する時に、ポンプを Off するために Device Shadow を更新する。 |
PumpOn |
ポンプ On の状態。本状態に遷移する時に、ポンプを On するために Device Shadow を更新する。また、ポンプ On 状態が継続しないように、タイマを使用して、本状態が 1 分間継続すると、ポンプ Off を指示して、Wait 状態に遷移する。 |
Wait |
ポンプを On した後に、一定のポンプ OFF の期間を設けるための Wait 状態 |
5. デバイス側の実装
マイコンからクラウドに接続したり、MQTT のメッセージを送受信するなどのコードをフルスクラッチで開発するのは、非常に難易度が高い実装となります。ですので、今回は、ESP32 マイコンの開発元の半導体メーカーである Espressif Systems 社が GitHub に公開している ESP-AWS-IoT という SDK を活用します。
ESP-AWS-IoT は AWS IoT Core を ESP32 デバイスで利用するためのサンプルコードやライブラリを含むオープンソースプロジェクトです。ESP-IDF フレームワークを使用し、AWS IoT の機能と統合されたセキュアな IoT ソリューションを構築するためのリソースが提供されています。
今回は ESP-AWS-IoT のサンプルとして収録されている thing_shadow を活用します。thing_shadow サンプルは、ESP32 デバイスで AWS IoT Thing Shadow を操作する実用的なコードです。Thing Shadow を介してデバイスの状態を同期し、セキュアで効果的なリモートデバイス管理を実現しています。今回開発するコードと内容が近いですね。
デバイス側の処理は、大きく分けると ①水分量計のセンサーデータをクラウドに Publish する処理と、②クラウドからの指示に従ってポンプを制御する処理の 2 つに大別されます。今回は、これらを FreeRTOS のタスクを使って実装します。タスクとは、RTOS(Real-Time Operating System)内で実行されるスレッドに相当します。独自のスタックとコンテキストを持ち、優先度や実行周期を設定でき、マルチタスク環境で並行して動作し、リアルタイムな処理やタイミングに敏感なアプリケーションを構築するために利用されます。
前述の 2 つ (①と②) の処理をそれぞれ xTaskSensor タスクと xTaskPump タスクという名称にします。
5-1. 水分量センサー用のタスク : xTaskSensor の実装
処理の概要を図に示します。このタスクの処理は非常にシンプルです。水分量計のデータを GPIO 経由で取得し、そのデータを AWS IoT Core に Publish するのみです。
参考までにサンプルのソースコードも併せて掲載します。

xtask_sensor.h
サンプルソースコード
#ifndef MAIN_XTASKSENSOR_H_
#define MAIN_XTASKSENSOR_H_
void xTaskSensor(void *pvParameters);
#endif /* MAIN_XTASKSENSOR_H_ */
xtask_sensor.c
サンプルソースコード
#include <stdio.h>
#include <inttypes.h>
#include <string.h>
#include <math.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <driver/spi_master.h>
#include <driver/gpio.h>
#include "esp_log.h"
#include "xtask_sensor.h"
#include "driver/gpio.h"
#include "driver/adc.h"
#include "shadow_demo_helpers.h"
#include "core_mqtt.h"
#include "xtask_pump.h"
#define TAG "xTaskSensor"
static MQTTPublishInfo_t publishInfo = {0};
static uint16_t GetMoisture()
{
uint16_t moisture = adc1_get_raw(ADC1_CHANNEL_4); // GROVE端子を使用 GP32
return moisture;
}
static void PublishMoisuture(uint16_t moisture)
{
MQTTContext_t *pMqttContext;
MQTTStatus_t status;
uint16_t packetId;
static char payload[50];
memset(payload, 0x00, sizeof(payload));
// This context is assumed to be initialized and connected.
pMqttContext = GetMqttContext();
LogInfo( ( "publishPumpState pContext(4): %p", pMqttContext) );
LogInfo( ( "publishPumpState pContext->getTime(4): %p", pMqttContext->getTime ) );
// QoS of publish.
publishInfo.qos = MQTTQoS0;
publishInfo.pTopicName = "watering-machine/state";
publishInfo.topicNameLength = strlen(publishInfo.pTopicName);
sprintf(payload, "{\"moisture\":%d}", moisture);
publishInfo.pPayload = payload;
publishInfo.payloadLength = strlen(payload);
// Packet ID is needed for QoS > 0.
packetId = MQTT_GetPacketId(pMqttContext);
LogInfo(("PublishMoisuture Packet ID: %u !!", packetId));
status = MQTT_Publish(pMqttContext, &publishInfo, packetId);
LogInfo(("Publish Result: %u.", status));
}
void xTaskSensor(void *pvParameters)
{
uint16_t moisture = 0;
while (1)
{
ESP_LOGI(TAG, "xTaskSensor is running...");
moisture = GetMoisture();
ESP_LOGI(TAG, "Moisture: %u", moisture);
PublishMoisuture(moisture);
vTaskDelay(5000 / portTICK_PERIOD_MS);
}
}
ポンプ制御用のタスク : xTaskPump の実装
処理の概要を下図に示します。xTaskPump では、Device Shadow の差分 (Delta) を取得するために使用されるトピック をSubscribe します。その後、周期的にメッセージを受信しているか確認します。この確認には Device SDK の MQTT_ReceiveLoop 関数を使用します。
もし、何らかのメッセージを受信している場合には、MQTT セッションの確立時に設定した コールバック関数 (eventCallback) が呼び出されます。eventCallback 関数では、受信したメッセージが、Device Shadow の Delta であることことを確認し、変更後のポンプの状態 (state.pumpOn) を読み出して、「理想的なポンプの状態」を表すフラグ (desiredPumpState フラグ) を更新します。
今回は、これらの eventCallback 関数や updateDeltaHandler 関数は、サンプルとして提供されていた main/shadow_demo_main.c のコードに実装されている同名の関数を参考にしました。参考までに、ソースコードを記載します。

xtask_pump.h
サンプルソースコード
#ifndef MAIN_XTASKPUMP_H_
#define MAIN_XTASKPUMP_H_
#include "core_mqtt.h"
void xTaskPump(void *pvParameters);
void eventCallback( MQTTContext_t * pMqttContext,
MQTTPacketInfo_t * pPacketInfo,
MQTTDeserializedInfo_t * pDeserializedInfo );
#define PUMP_ON 1
#define PUMP_OFF 0
#endif /* MAIN_XTASKPUMP_H_ */
xtask_pump.c
サンプルソースコード
#ifndef MAIN_XTASKPUMP_H_
#define MAIN_XTASKPUMP_H_
#include "core_mqtt.h"
void xTaskPump(void *pvParameters);
void eventCallback( MQTTContext_t * pMqttContext,
MQTTPacketInfo_t * pPacketInfo,
MQTTDeserializedInfo_t * pDeserializedInfo );
#define PUMP_ON 1
#define PUMP_OFF 0
#endif /* MAIN_XTASKPUMP_H_ */
5-2. ビルド手順
ビルド方法ですが、私は VisualStudio Code 用に Espressif Systems 社が提供している vscode-esp-idf-extension (VS Code の拡張) を使用しました。vscode-esp-idf-extension は、ESP-IDF(Espressif IoT Development Framework)用のVS Code 拡張機能です。ESP32/ESP8266 の開発をサポートし、自動補完、ビルド、デバッグ、フラッシュの機能を提供します。セットアップ方法は vscode-esp-idf-extension の README を参照してください。では、ビルド方法を以下に記載します。
1. ESP-AWS-IOTリポジトリの README に記載されている内容を参考にして、リポジトリを開発環境にクローンします
2. examples/thing_shadow のサンプルコードに変更を加えていきます。
初期的なフォルダ構成
初期的なフォルダ構成は下記のようになっていると思います。まず、ここまでに説明した 2 つのタスク処理 (xTaskSensor, xTaskPump) のソースコードをファイルとして保存します。例えば、 xtask_pump.c/xtask_pump.h, xtask_sensor.c/xtask_sensor.h とします。これらのファイルを main フォルダの配下に保存します。併せて、main フォルダ配下の CMakeLists.txt ファイルを更新し、追加したソースコードをビルドの対象に含めます。ルートディレクトリの直下にも同名の CMakeLists.txt が存在しますので、間違えないように注意してください。
.
├── CMakeLists.txt
├── README.md
├── main
│ ├── CMakeLists.txt
│ ├── Kconfig.projbuild
│ ├── app_main.c
│ ├── certs
│ │ ├── client.crt
│ │ ├── client.key
│ │ └── root_cert_auth.crt
│ ├── demo_config.h
│ ├── idf_component.yml
│ ├── shadow_demo_helpers.c
│ ├── shadow_demo_helpers.h
│ └── shadow_demo_main.c
├── partitions.csv
└── sdkconfig.defaults
コードを変更
3. つづいて、examples/thing_shadow/main/app_main.c にて、下記を実行できるようにコードを変更します。後続のサンプルコードを参照してください。
ハードウェアの接続状態に合わせて GPIO を設定する
examples/thing_shadow/main/shadow_demo_helpers.c に収録されている EstablishMqttSession を呼び出して、MQTT セッションを確立する
xTaskCreate 関数を使って開発した2つのタスクを作成する
コードを変更
サンプルコード
examples/thing_shadow/main/app_main.c
void app_main()
{
/* 省略 */
/* 下記の関数呼び出しをコメントアウトし、それ以降のコードを追記 */
/* aws_iot_demo_main(0,NULL); */
int returnStatus = EXIT_SUCCESS;
/* GPIOの設定関数を呼び出し */
SetupGpio();
do
{
/* MQTTセッションを確立 */
returnStatus = EstablishMqttSession(eventCallback);
if(returnStatus == EXIT_FAILURE)
{
LogError( ( "Failed to connect to MQTT broker." ) );
}
else
{
/* 2つのタスクを作成する */
xTaskCreate(xTaskPump, "TaskPump", 1024*6, NULL, 2, NULL);
xTaskCreate(xTaskSensor, "TaskSensor", 1024*6, NULL, 2, NULL);
}
} while(returnStatus != EXIT_SUCCESS);
}
証明書、秘密鍵を配置
前の手順で取得した証明書、秘密鍵をそれぞれ client.crt, client.key というファイル名で main/certs フォルダに配置します。なお、それぞれ、既にサンプルの client.crt, client.key ファイルが配置されているので、上書き保存などして配置してください。
ESP-IDF 拡張の設定
ESP-IDF 拡張の設定を更新していきます。画面下部の歯車マークをクリックすると、ESP-IDF の設定画面が表示されます。

Example Configuration
6. 左メニューの「Example Configuration」を選択し、前の手順で調べた AWS IoT Core のエンドポイントと、登録した Thing 名を設定します。

Example Connection Configuration
7. 続いて、左メニューの「Example Connection Configuration」を選択し、Wifi SSID およびパスワードを設定し、その後、設定を保存 (「Save」)」します。

(オプション) M5StickC Plus に書き込む
8. (オプション) M5StickC Plus に書き込む際のボーレートですが、私は .vscode/settings.json ファイルに "idf.flashBaudRate": "115200", と追記しました。
{
"idf.port": "/dev/cu.usbserial-xxxxxxxxxx",
"idf.flashBaudRate": "115200",
"idf.flashType": "UART"
}
設定完了
9. これで設定ができましたので、デバイスを PC と接続し、ソースコードをビルドしてデバイスに書き込むことができると思います。ESP-IDF 拡張の基本的な使用方法が こちら で説明されていますので、参照してください。
6. 試してみる
水やりの要否を判断するための水分量センサーの閾値などを調整し、システムを完成させました。種をまいたプランターの土に水分量計を指し、ポンプを設置して、動作確認してみました。仕様通りに正常に動作しているようです。
念の為、エラーケースもテストしてみました。水分量センサーを土から取り出し、「土が乾いている状態」を継続させてみました。しばらくすると、ポンプが作動し、水が供給され始め、1 分後には停止しました。
う〜ん、1 分間も水を供給するのは長すぎますね。ただし、IoT Events のタイマーの最小設定時間が 60 秒という制約があります。これでは、フェールセーフ機能としては、いまいちなので、マイコンのプログラムで時間制限を組み込んだ方が良さそうですね。
7. 注意事項
今回開発したシステムは、あくまで学習目的で開発したものです。エラー処理はログの出力のみ、など、最低限の実装に留めています。商用のシステム開発の場合には、エラー処理など、もっと洗練化する必要がある部分がありますので、ご注意ください。
また、もしデバイスがオフラインだった時にクラウド側で変更があった場合には、デバイス側は知ることができません。ですので、デバイス起動時に Shadow の値を取得するなど、何かしらの方法で現在の値を取得する必要があります。
8. 今後の展望
これで、最低限の水やり機能は実現できました。今後は、以下のことにもトライしていきたいと考えています。
- 水分量だけでなく温度・湿度も測定し、最適な水やりの条件を探ってみる
- 測定したセンサー情報をグラフ化し可視化する
こう言った機能の追加は、例えばサンサー情報を新たに取得できるようにして、既存のタスクまたは、既存のタスクを横展開して、新たにタスクを追加する、など簡単に拡張できるようになりました。
9. まとめ
いかがでしたでしょうか ? 今回のようにマイコンや、ノーコードで開発可能な AWS サービスを活用することにより、安価に、そしてコード量を減らして迅速にシステムを開発可能であることがお分かりいただけたかと思います。また、マイコンでの開発は、比較的に敷居が高い印象をお持ちの方もいらっしゃるかと思いますが、FreeRTOS や SDK を活用することにより、シンプルに実装できることもご理解いただけたかと思います。
今後も開発をマイペースに続けていき、もっとリッチなシステムを作っていきたい、と考えています。
筆者プロフィール
宮本 篤
アマゾン ウェブ サービス ジャパン合同会社
プロフェッショナルサービス本部 IoT コンサルタント
主に IoT 関連を中心に、クラウドの活用を目指すお客様に対して、ビジネス目標の達成をご支援しております。趣味のブラジリアン柔術で体を鍛え、抜けない筋肉痛を抱えながら日々の業務に従事しています。
