スマートガーデニングの実現へ ! 

マイコンを活用したリーズナブルなお花の自動水やり機

2024-03-04
日常で楽しむクラウドテクノロジー

Author : 宮本 篤

こんにちは。IoT コンサルタントの宮本です。

先日、とあるイベントでお花の種を頂きました。私は、お花は鑑賞するのは好きなのですが、育てた経験はほとんどありません。ネットで軽く、お花の育て方を調べてから、種を蒔き、育ててみました。無事に芽は出てきたのですが、「水やりって、どのようなタイミングでやれば良いの ?」と疑問を持ちました。また、水やりを忘れてしまう日もあって、これは良くない、何とかしようと思い、「自動水やりシステム」を開発することにしました !

まずは、どんな機能を実現するのか、を明確にします。理想的には、下記のようなシステムを作りたい、と考えていました。

  • 土壌の水分量に基づいて、水を供給 / 停止する
  • 水分量に加えて、気温・湿度もインプット情報に加える
  • 花の育成状況をカメラで撮影し、日々の状態を記録する
  • 更に、機械学習を使って、撮像より、植物の成長度合いを解析し、最適な水やり周期にフィードバックする

全てを一度に作るのはハードルが高そうなので、今回は開発のハードルを下げて、1 番目の「土壌の水分量に基づいて、水を供給/停止する」のみを実現することにしました。

このクラウドレシピ (ハンズオン記事) を無料でお試しいただけます »

毎月提供されるクラウドレシピのアップデート情報とともに、クレジットコードを受け取ることができます。 


1. 開発の方針

前述のようなシンプルな仕様であれば、システムの作り方には様々な方法が考えられるかと思います。今回は、下記の方針を立て、開発することにしました。

1. できるだけデバイスを安価にする

デバイス側の選択肢には PC、ラズパイ、マイコンなどありますが、単純にデバイス単体の価格を比較すると、やはりマイコンが一番安価になります。ですので、デバイスにはマイコンを採用しました。

2. 柔軟性を向上さる

今回のように、水の供給/停止を判断するための水分量の閾値などは、試しながら調整することが想定されます。マイコンを使う場合には、デバイス側のソースコードの更新は、クラウド側に比べると比較的に作業コストがかかる場合があります。そのため、マイコン側のソースコードを更新するよりも、クラウド側を更新する方がシステムとしての柔軟性を高められる、と考え、システムの変動部は可能な限りクラウド側で実現する方針にしました。

3. 開発工数を削減する

機能を実現するためには、基本的にはソースコードでロジックを組む必要があります。一方で、AWS にはノーコードで機能を実現可能なサービスも存在しています。よって、クラウド側は、できるだけノーコードで開発し、開発工数を削減しました。

開発の方針をまとめます。

  • 「できるだけデバイスを安価にする」: デバイスには安価なマイコンを使用する
  • 「柔軟性を向上さる」: 変動部やロジックは、できるだけクラウド側で実現する
  • 「開発工数を削減する」: クラウド側はできるだけノーコードで開発する

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 の設定

デバイスが AWS IoT Core に接続するためには、モノの登録やデバイス証明書の発行などが必要となります。まずは、クラウド側を設定していきます。

3-1. AWS IoT のモノの作成

デバイスを AWS IoT Core に接続するためにモノ (Thing) を作成します。AWS IoT Core のマネジメントコンソールから IoT のモノを作って、MQTT で Publish や Subscribe するためのポリシーを追加した後、接続に必要な証明書ファイルや秘密鍵ファイルを取得します。

  • AWS IoT Core のマネジメントコンソールから「モノ」を開きます。
    • モノを作成」ボタンを選択します。
    • 1 つのモノを作成」を選択します。
    • モノのプロパティを指定 画面では、 モノの名前m5stickc-plus とし、残りはデフォルトのままで 画面下部の「次へ」ボタンをクリックします。
  • デバイス証明書を設定 - オプション 画面が表示されます。
    • デバイス証明書 セクション にて「新しい証明書を自動生成 (推奨)」を選択します。
    • 次へ」ボタンをクリックします。
  • 証明書にポリシーをアタッチ - オプションの画面が表示されます。
    • ポリシー で 「ポリシーを作成」を選択します。
  • ブラウザの新規タブが開き、ポリシーを作成 の画面が表示されます
    • ポリシー名に m5stickc-plus-policy と入力します。
    • ポリシーステートメント」タブを選択し、「新しいステートメント」をクリックして、下記を設定していきます。本例では、東京リージョン (ap-northeast-1) を使用していますが、他のリージョンを使用する場合には、変更してください。また、アカウントIDは、ご自身の環境に合わせて変更してください。
    • 作成」ボタンを選択します。
ポリシー効果 ポリシー
アクション
ポリシーリソース
許可 iot:Connect arn:aws:iot:ap-northeast-1:{アカウントID}:client/m5stickc-plus
許可 iot:Publish arn:aws:iot:ap-northeast-1:{アカウントID}:topic/watering-machine/state,
arn:aws:iot:ap-northeast-1:{アカウントID}:topic/$aws/things/m5stickc-plus/shadow/update
許可 iot:Subscribe arn:aws:iot:ap-northeast-1:{アカウントID}:topicfilter/$aws/things/m5stickc-plus/shadow/update/delta
許可 iot:Receive arn:aws:iot:ap-northeast-1:{アカウントID}:topic/$aws/things/m5stickc-plus/shadow/update/delta
  • 証明書にポリシーをアタッチ - オプションの画面があるタブに戻ります。
    • さきほど作成した m5stickc-plus-policy を選択します。
    • モノを作成」ボタンを選択します。
  • 証明書とキーをダウンロード のモーダル画面が表示されます。
    • 以下のファイルをダウンロードします。
      • デバイス証明書 - <long-string>-certificate.pem.crt
      • プライベートキーファイル - <long-string>-private.pem.key
  • ダウンロードしたファイルは、後続の手順で使用します。

3-2. AWS IoT のルールの作成ンプルコードを GitHub から取得

AWS IoT Core が受け取ったメッセージをそのまま AWS IoT Events に渡すようにルールを設定します。AWS IoT Core デベロッパーガイドの「AWS IoT ルールの作成」を参考にルールを作成します。今回は、デバイスが Publish するメッセージを全て AWS IoT Events に渡すように設定します。

  • SQL ステートメント : SELECT * FROM ‘watering-machine/state’
  • Actions : Send a message to an IoT Events Input

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
}

この 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 を作成します。

ここではロール名を 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 に置き換えます。

{
    "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

#include <stdio.h>
#include <inttypes.h>
#include <string.h>
#include <math.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#include <driver/gpio.h>
#include "esp_log.h"
#include "shadow_demo_helpers.h"
#include "shadow.h"
#include "core_json.h"

#include "xtask_pump.h"

#define TAG "xTaskPump"

extern gpio_num_t PUMP_GPIO;
static uint8_t desiredPumpState = PUMP_OFF;
static MQTTPublishInfo_t publishInfo = {0};
static MQTTSubscribeInfo_t pSubscriptionList[1];

static void ControlPump(uint8_t pump_state)
{
    gpio_set_level(PUMP_GPIO, pump_state);
}

static void updateDeltaHandler(MQTTPublishInfo_t *pPublishInfo)
{
    char *outValue = NULL;
    uint32_t outValueLength = 0U;
    JSONStatus_t result = JSONSuccess;

    assert(pPublishInfo != NULL);
    assert(pPublishInfo->pPayload != NULL);

    LogInfo(("/update/delta json payload:%s.", (const char *)pPublishInfo->pPayload));

    /* Make sure the payload is a valid json document. */
    result = JSON_Validate((const char *)pPublishInfo->pPayload,
                           pPublishInfo->payloadLength);

    if (result == JSONSuccess)
    {
        /* Get powerOn state from json documents. */
        result = JSON_Search((char *)pPublishInfo->pPayload,
                             pPublishInfo->payloadLength,
                             "state.pumpOn",
                             sizeof("state.pumpOn") - 1,
                             &outValue,
                             (size_t *)&outValueLength);
    }
    else
    {
        LogError(("The json document is invalid!!"));
    }

    if (result == JSONSuccess)
    {
        /* Convert the powerOn state value to an unsigned integer value. */
        desiredPumpState = (uint8_t)strtoul(outValue, NULL, 10);
        LogInfo(("The new pumpOn state desiredPumpState:%" PRIu8 " \r\n",
                 desiredPumpState));
    }
    else
    {
        LogError(("pumpOn value does not exist in the message."));
    }
}

void eventCallback(MQTTContext_t *pMqttContext,
                          MQTTPacketInfo_t *pPacketInfo,
                          MQTTDeserializedInfo_t *pDeserializedInfo)
{
    LogInfo(("eventCallback is running"));

    ShadowMessageType_t messageType = ShadowMessageTypeMaxNum;
    const char *pThingName = NULL;
    uint8_t thingNameLength = 0U;
    const char *pShadowName = NULL;
    uint8_t shadowNameLength = 0U;

    (void)pMqttContext;

    assert(pDeserializedInfo != NULL);
    assert(pMqttContext != NULL);
    assert(pPacketInfo != NULL);

    if ((pPacketInfo->type & 0xF0U) == MQTT_PACKET_TYPE_PUBLISH)
    {
        assert(pDeserializedInfo->pPublishInfo != NULL);
        LogInfo(("pPublishInfo->pTopicName:%s.", pDeserializedInfo->pPublishInfo->pTopicName));

        /* Let the Device Shadow library tell us whether this is a device shadow message. */
        if (SHADOW_SUCCESS == Shadow_MatchTopicString(pDeserializedInfo->pPublishInfo->pTopicName,
                                                      pDeserializedInfo->pPublishInfo->topicNameLength,
                                                      &messageType,
                                                      &pThingName,
                                                      &thingNameLength,
                                                      &pShadowName,
                                                      &shadowNameLength))
        {
            /* Upon successful return, the messageType has been filled in. */
            if (messageType == ShadowMessageTypeUpdateDelta)
            {
                /* Handler function to process payload. */
                updateDeltaHandler(pDeserializedInfo->pPublishInfo);
            }
            else
            {
                LogInfo(("Other Shadow message type:%d !!", messageType));
            }
        }
        else
        {
            LogError(("Shadow_MatchTopicString parse failed:%s !!", (const char *)pDeserializedInfo->pPublishInfo->pTopicName));
        }
    }
    else
    {
        LogInfo(("Other MQTT message type:%d !!", pPacketInfo->type));
    }
}

static void PublishPumpState(uint8_t pump_state)
{
    MQTTContext_t *pMqttContext;
    MQTTStatus_t status;
    uint16_t packetId;
    static char payload[50];
    memset(payload, 0x00, sizeof(payload));

    pMqttContext = GetMqttContext();
    publishInfo.qos = MQTTQoS0;
    publishInfo.pTopicName = "$aws/things/m5stickc-plus/shadow/update";
    publishInfo.topicNameLength = strlen(publishInfo.pTopicName);
    sprintf(payload, "{\"state\":{\"reported\": {\"pumpOn\": %d}}}", pump_state);
    publishInfo.pPayload = payload;
    publishInfo.payloadLength = strlen(payload);

    packetId = MQTT_GetPacketId(pMqttContext);
    status = MQTT_Publish(pMqttContext, &publishInfo, packetId);
    LogInfo(("Publish Result: %u.", status));
}

static MQTTStatus_t SubscribePumpDelta(void)
{
    MQTTContext_t *pMqttContext;
    MQTTStatus_t mqttStatus;
    uint16_t subscribePacketId;
    const char *TopicFilter = "$aws/things/m5stickc-plus/shadow/update/delta";
    uint16_t topicFilterLength = strlen(TopicFilter);

    pMqttContext = GetMqttContext();

    pSubscriptionList[0].qos = MQTTQoS0;
    pSubscriptionList[0].pTopicFilter = TopicFilter;
    pSubscriptionList[0].topicFilterLength = topicFilterLength;

    /* Generate packet identifier for the SUBSCRIBE packet. */
    subscribePacketId = MQTT_GetPacketId(pMqttContext);

    /* Send SUBSCRIBE packet. */
    mqttStatus = MQTT_Subscribe(pMqttContext,
                                pSubscriptionList,
                                sizeof(pSubscriptionList) / sizeof(MQTTSubscribeInfo_t),
                                subscribePacketId);
    return mqttStatus;
}

void xTaskPump(void *pvParameters)
{
    uint8_t pump_state = PUMP_OFF;
    MQTTStatus_t mqttStatus;
    MQTTContext_t *pMqttContext;

    /* 初期設定はポンプ OFF */
    ControlPump(pump_state);

    /* Shadow の Delta を Subscribe */
    mqttStatus = SubscribePumpDelta();

    /* MQTTのContextを取得 */
    pMqttContext = GetMqttContext();
    /* ポンプの初期状態を Shadow に Report */
    PublishPumpState(pump_state);

    while (1)
    {
        vTaskDelay(500 / portTICK_PERIOD_MS);

        mqttStatus = MQTT_ReceiveLoop(pMqttContext);
        ESP_LOGI(TAG, "xTaskPump: MQTT_ReceiveLoop status is %s \r\n", MQTT_Status_strerror(mqttStatus));

        if (pump_state != desiredPumpState)
        {
            pump_state = desiredPumpState;
            ControlPump(pump_state);
            PublishPumpState(pump_state);
        }
    }
}

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);
}

4. 前の手順で取得した証明書、秘密鍵をそれぞれ client.crt, client.key というファイル名で main/certs フォルダに配置します。なお、それぞれ、既にサンプルの client.crt, client.key ファイルが配置されているので、上書き保存などして配置してください。

5. ESP-IDF 拡張の設定を更新していきます。画面下部の歯車マークをクリックすると、ESP-IDF の設定画面が表示されます。

画像をクリックすると拡大します

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

画像をクリックすると拡大します

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

画像をクリックすると拡大します

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 を活用することにより、シンプルに実装できることもご理解いただけたかと思います。

今後も開発をマイペースに続けていき、もっとリッチなシステムを作っていきたい、と考えています。


builders.flash メールメンバーへ登録することで
AWS のベストプラクティスを毎月無料でお試しいただけます

筆者プロフィール

宮本 篤
アマゾン ウェブ サービス ジャパン合同会社
プロフェッショナルサービス本部 IoT コンサルタント

主に IoT 関連を中心に、クラウドの活用を目指すお客様に対して、ビジネス目標の達成をご支援しております。趣味のブラジリアン柔術で体を鍛え、抜けない筋肉痛を抱えながら日々の業務に従事しています。

AWS を無料でお試しいただけます

AWS 無料利用枠の詳細はこちら ≫
5 ステップでアカウント作成できます
無料サインアップ ≫
ご不明な点がおありですか?
日本担当チームへ相談する