【重要】LINBLE-Z2量産販売開始およびLINBLE-Z1使用部品変更予定のお知らせ

M5StackでBluetooth® LE通信を実現するまでの手順メモ⑦家庭菜園のIoT化編

こんにちは、ムセンコネクトCCOの伊藤です。(プロフィール紹介はこちら

M5StackでBluetooth® LE(BLE)通信を実現するまでの手順メモ」の第7回目です。今回は「家庭菜園のIoT化編」です。

ご注意ください

M5Stack自体はBluetooth認証を取得していないため、M5Stackで開発した最終製品のBluetooth認証取得は困難な場合があります。M5Stackを用いた製品化をご検討の際は、この点に十分ご注意ください。

M5Stackの公式サイトで製品を検索しても「Bluetooth」というキーワードでは見つからない
目次

スマート農業のような家庭菜園のIoT化

第7回でもM5Stackに接続したセンサーから取得した値をアドバタイズデータとして送信します。前回は「工場などに設置されている積層信号灯を後付けでIoT化する手順」をイメージしたものでしたが、今回はスマート農業のような家庭菜園のIoT化をイメージしながら実現していきます。

スマート農業のような家庭菜園のIoT化を想定し、以下のセンサーを選定しました。

  • 環境光センサーUnit DLight ): 照度を測定し、野菜が浴びる太陽光の量を把握するために選定しました。
  • 土壌水分センサーUnit Earth ): 土壌の水分含有量を測定し、水やりの適切なタイミングを判断するために選定しました。

環境光センサーは赤色のPort.A(I2C)コネクタですが、土壌水分センサーはPort.B(GPIO)コネクタですので、拡張ベース(Base DIN )も合わせて準備しました。

各センサー、拡張ベースとCoreS3 SE

どちらのセンサーも、サンプルコードが公開されてはいますが、あえて利用せず今回もAIである Gemini 2.5 Pro を利用してコーディングの支援をしてもらいます。繰り返しになりますが、AIの生成コードは過信しすぎないようにしましょう。

プロンプト指示(まずは環境光センサーを動かす)

まずは、前回同様Bluetooth LE(BLE)通信から離れて、センサー単体の動作確認を行います。まずは環境光センサーを試します。業務で「BH1750FVI」は何度も使ってきましたが、AIに任せます。

M5Stackのディスプレイで目視確認ができる程度の簡単な動作で済ませたいと思います。

仕様として与えたプロンプトは以下のとおりです。

M5CoreS3 SE、M5Stack用環境光ユニット(BH1750FVI搭載)が手元にある。
再度Bluetooth LE通信から離れ、環境光ユニットの動作確認を行いたい。

M5Stack用環境光ユニット(BH1750FVI搭載)の詳細は以下のとおりである。
https://docs.m5stack.com/en/unit/DLight%20Unit

仕様を以下に示す。
仕様を満たすプログラムコードを提示してほしい。

開発環境
・Arduino IDE

目的
・M5Stack用環境光ユニット(BH1750FVI搭載)の動作を確認する

機能
1.センサデータ取得
・M5StackのPortAに接続する
・電源投入直後から1秒毎にセンシングする

2.ディスプレイ表示
・センシングした値をディスプレイにテキスト表示する

Arduinoスケッチ(プログラムコード)が生成されるだけでなく、プログラムを動作させるためのライブラリについても指南してくれました。

承知いたしました。
M5CoreS3 SEと環境光ユニット(BH1750FVI)の動作確認を行うためのプログラムを作成します。

ご提示の仕様を満たす、M5CoreS3 SE用のArduinoプログラムを以下に示します。
このプログラムを利用するには、事前にArduino IDEのライブラリマネージャから
**BH1750**ライブラリをインストールしてください。

生成されたArduinoスケッチ(プログラムコード)は以下のとおりです。

/**
 * @file M5CoreS3_Ambient_Light_Test.ino
 * @author Gemini
 * @brief M5CoreS3 SEと環境光ユニット(BH1750FVI)の動作確認プログラム
 * @version 1.0
 * @date 2025-08-05
 * @copyright Copyright (c) 2025
 *
 * @note
 * このプログラムを使用する前に、Arduino IDEのライブラリマネージャで
 * "BH1750 by Christopher Laws" を検索し、インストールしてください。
 *
 * 機能:
 * 1. PortAに接続された環境光センサから1秒毎にデータを取得する。
 * 2. 取得した照度(lux)をディスプレイに表示する。
 */


#include <M5Unified.h>
#include <Wire.h>
#include <BH1750.h>


// 環境光センサのインスタンスを作成
BH1750 lightMeter;


// 最後にセンサを読み取った時刻を保存する変数
unsigned long lastReadTime = 0;


/**
 * @brief 初期設定
 */
void setup() {
  // M5Stackの初期化
  M5.begin();
  
  // シリアル通信の開始
  Serial.begin(115200);


  // M5CoreS3のPortAはI2CピンがG2(SDA), G1(SCL)なので、Wireを初期化
  // M5.begin()で自動的に設定されることが多いですが、明示的に指定します。
  Wire.begin(2, 1);


  M5.Display.setTextSize(2);
  M5.Display.println("Ambient Light Sensor Test");


  // 環境光センサの初期化
  if (lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE, 0x23, &Wire)) {
    Serial.println(F("BH1750 Advanced begin"));
    M5.Display.println("Found BH1750 sensor");
  } else {
    Serial.println(F("Error initialising BH1750"));
    M5.Display.println("BH1750 not found...");
    while (1); // センサが見つからない場合はここで停止
  }


  // 画面を一度クリア
  delay(1500);
  M5.Display.fillScreen(BLACK);
}


/**
 * @brief メインループ
 */
void loop() {
  // 1秒ごとに処理を実行
  if (millis() - lastReadTime > 1000) {
    lastReadTime = millis(); // 時刻を更新


    // 照度(lux)を取得
    // 2.0.0以上のライブラリでは、readLightLevel()がboolを返す仕様に変更されたため
    // センサーが値を返す準備ができているか確認します。
    if (lightMeter.measurementReady()) {
      float lux = lightMeter.readLightLevel();


      // 画面をクリア
      M5.Display.fillScreen(BLACK);
      M5.Display.setCursor(0, 40);


      // ラベルを表示
      M5.Display.setTextSize(3);
      M5.Display.setTextColor(WHITE);
      M5.Display.println("Ambient Light:");


      // 照度の値を表示
      M5.Display.setTextSize(5);
      M5.Display.setTextColor(YELLOW);
      M5.Display.printf("%.2f lx", lux);
      
      // シリアルモニタにも出力
      Serial.printf("Light: %.2f lx\n", lux);
    }
  }
  M5.update();
}

環境光センサーの動作を確認する

CoreS3 SEに書き込みをして、動作を確認します。室内での確認のため、500ルクス程度しかありませんが、向きを変えるだけでかなり下がることがわかります。当然ですが、太陽光を測定するためには向きや遮蔽物がないことが大事ですね。

室内の環境光を取得してみる

センサーを開封して、プロンプトを考えて打ち込んで、ここまでで15分程度でしょうか。AIは自身の相棒として欠かせない存在といっても過言ではないですね。間違いを見抜く力を身につけていけば非常に強力なツールに間違いありません。

プロンプト指示(つぎは土壌水分センサーを動かす)

次に、土壌水分センサー単体の動作確認を行います。こちらも、M5Stackのディスプレイで目視確認ができる程度の簡単な動作で済ませたいと思います。

仕様として与えたプロンプトは以下のとおりです。

先ほどのM5CoreS3 SE、M5Stack用環境光ユニット(BH1750FVI搭載)に加えて、
M5Stack用土壌水分センサユニット(Unit Earth)も手元にある。
先ほどの動作に加えて土壌水分センサの動作確認を行いたい。

M5Stack用土壌水分センサユニット(Unit Earth)の詳細は以下のとおりである。
https://docs.m5stack.com/en/unit/earth

仕様を以下に示す。
仕様を満たすプログラムコードを提示してほしい。

開発環境
・Arduino IDE

目的
・M5Stack用環境光ユニット(BH1750FVI搭載)の動作を確認する
・M5Stack用土壌水分センサユニット(Unit Earth)の動作を確認する

機能
1.センサデータ取得
・M5StackのPortAに環境光ユニットを接続する
・M5StackのPortBに土壌水分センサユニットを接続する
・電源投入直後から1秒毎にセンシングする

2.ディスプレイ表示
・センシングした値をディスプレイにテキスト表示する
・土壌水分センサはアナログ値、デジタル値それぞれを表示し、可能であれば相対湿度も表示してほしい

3.他
・土壌水分センサから得られた値から湿度に換算する計算式などコメントに残すこと

環境光センサー向けのスケッチ(プログラムコード)を変更する形で生成してくれました。

私がプロンプトに「相対湿度」を表示するよう記載したため、アナログ値、デジタル値、相対湿度が追加で表示されるようになりました。それ自体は私の指示によるものなので間違いではないのですが、水に浸けて確認したところ、このセンサーは、土壌に刺して土壌表層からどこまで乾いているのかを判断するために利用するものではないか?と考え直しました。ですので、水中と乾燥(何にも触れていない状態)のアナログ値をキャリブレーション値として与えつつ、相対湿度、というのは違和感を感じることを再度プロンプトで指示します。

const int DRY_VALUE = 4095; // 例: 乾燥状態のアナログ値
const int WET_VALUE = 2100; // 例: 水中でのアナログ値

この値を暫定値とします。
ただ、このセンサで相対湿度を出すのは少し意図が異なる気もしてきました。
センサを刺して、土壌表層からどこまで乾いているか、を判断するためのセンサのような気がしています。
「相対湿度」という表現に関するご指摘、ありがとうございます。
まさしくその通りで、このセンサーは静電容量の変化を捉えており、土壌の相対的な水分量や、
どこまで湿っているかを判断するのに適しています。一般的な「空気中の相対湿度」とは意味合いが異なりますね。

この洞察を反映し、より実態に合うように、
プログラム内の「Humidity」という表示を「Moisture」という表現に修正しました。

私の違和感に寄り添って、Moisuture というしっくりくるフレーズで対応してくれました。

生成されたArduinoスケッチ(プログラムコード)は以下のとおりです。

/**
 * @file M5CoreS3_Sensor_Test.ino
 * @author Gemini
 * @brief M5CoreS3 SEで環境光ユニットと土壌水分センサユニットの動作を確認するプログラム
 * @version 2.2
 * @date 2025-08-06
 * @copyright Copyright (c) 2025
 *
 * @note
 * v2.2: 土壌水分センサーのキャリブレーション値更新。「Humidity」の表示を「Moisture」に修正。
 * v2.1: 土壌水分センサーのキャリブレーション値を暫定値に更新。
 * v2.0: M5Stack用土壌水分センサユニット(Unit Earth)の読み取り機能を追加。
 * v1.0: 環境光ユニット(BH1750FVI)の動作確認プログラム。
 *
 * ライブラリ:
 * このプログラムを使用する前に、Arduino IDEのライブラリマネージャで
 * "BH1750 by Christopher Laws" を検索し、インストールしてください。
 *
 * 機能:
 * 1. PortAの環境光センサ、PortBの土壌水分センサから1秒毎にデータを取得する。
 * 2. 取得した照度(lux)、土壌水分(アナログ/デジタル/水分量)をディスプレイに表示する。
 */


#include <M5Unified.h>
#include <Wire.h>
#include <BH1750.h>


// --- センサーのインスタンスを作成 ---
BH1750 lightMeter;


// --- PortB (土壌水分センサ) のピン定義 ---
// M5CoreS3 SEのPortBは G8(Analog), G9(Digital)
const int SOIL_ANALOG_PIN = 8;
const int SOIL_DIGITAL_PIN = 9;


// --- 土壌水分の換算のためのキャリブレーション値 ---
// これらの値は環境によって変わるため、より正確な値が必要な場合は調整してください。
// 1. センサーを空気中で乾燥させた状態のアナログ値をDRY_VALUEに設定
// 2. センサーを水の入ったコップに入れた状態のアナログ値をWET_VALUEに設定
const int DRY_VALUE = 4095; // 乾燥状態のアナログ値 (暫定値)
const int WET_VALUE = 2100; // ★修正: 水中でのアナログ値 (暫定値)


// 最後にセンサを読み取った時刻を保存する変数
unsigned long lastReadTime = 0;


/**
 * @brief 初期設定
 */
void setup() {
  M5.begin();
  Serial.begin(115200);


  // --- PortA (環境光センサ) の設定 ---
  Wire.begin(2, 1); // PortAのI2Cピン (G2, G1)
  M5.Display.setTextSize(2);
  M5.Display.println("Sensor Test");


  if (lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE, 0x23, &Wire)) {
    Serial.println(F("BH1750 Advanced begin"));
    M5.Display.println("- Found BH1750 sensor");
  } else {
    Serial.println(F("Error initialising BH1750"));
    M5.Display.println("- BH1750 not found...");
  }


  // --- PortB (土壌水分センサ) の設定 ---
  pinMode(SOIL_ANALOG_PIN, INPUT);
  pinMode(SOIL_DIGITAL_PIN, INPUT);
  M5.Display.println("- Earth Unit setup done");


  delay(1500);
  M5.Display.fillScreen(BLACK);
}


/**
 * @brief メインループ
 */
void loop() {
  // 1秒ごとに処理を実行
  if (millis() - lastReadTime > 1000) {
    lastReadTime = millis(); // 時刻を更新


    // --- 1. センサーデータ取得 ---
    // 環境光センサ
    float lux = -1.0;
    if (lightMeter.measurementReady()) {
      lux = lightMeter.readLightLevel();
    }


    // 土壌水分センサ
    int soil_analog = analogRead(SOIL_ANALOG_PIN);
    int soil_digital = digitalRead(SOIL_DIGITAL_PIN);


    // 土壌の相対的な水分量(%)を計算
    // map()関数で、センサーのアナログ値の範囲(DRY_VALUE-WET_VALUE)を
    // 0-100%の範囲に変換します。
    // センサーは水分が多いほど値が小さくなるため、マッピング元は(DRY, WET)の順になります。
    int moisture = map(soil_analog, DRY_VALUE, WET_VALUE, 0, 100);
    // 0-100の範囲に収める
    moisture = constrain(moisture, 0, 100);




    // --- 2. ディスプレイ表示 ---
    M5.Display.fillScreen(BLACK);
    M5.Display.setCursor(10, 10);
    M5.Display.setTextSize(2);
    M5.Display.setTextColor(WHITE);


    // 環境光
    M5.Display.println("Ambient Light:");
    M5.Display.setCursor(30, 40);
    M5.Display.setTextColor(YELLOW);
    M5.Display.printf("%.2f lx\n\n", lux);


    // 土壌水分
    M5.Display.setTextColor(WHITE);
    M5.Display.setCursor(10, 100);
    M5.Display.println("Soil Moisture:");
    
    M5.Display.setTextColor(CYAN);
    M5.Display.setCursor(30, 130);
    M5.Display.printf("Analog : %d\n", soil_analog);
    
    M5.Display.setCursor(30, 160);
    // デジタル値は0(WET)か1(DRY)で返ってくる
    M5.Display.printf("Digital: %s\n", (soil_digital == 0) ? "WET" : "DRY");


    M5.Display.setCursor(30, 190);
    // ★修正: 表示を"Humidity"から"Moisture"に変更
    M5.Display.printf("Moisture: %d %%\n", moisture);




    // --- 3. シリアルモニタにも出力 ---
    Serial.printf("Light: %.2f lx, Soil Analog: %d, Soil Digital: %d, Moisture: %d%%\n",
                  lux, soil_analog, soil_digital, moisture);
  }
  M5.update();
}

土壌水分センサーの動作を確認する

外から土を持ってきて、水で湿らせた状態で土壌水分センサーを刺してみます。

良さそうです!土壌水分量(Moisture)の妥当性は継続して確認する必要がありますが、動作していることは確認できました。

また、環境光センサーも一緒に動作していることが確認できました。

実際に土に刺してみる

プロンプト指示(アドバタイズデータに載せて発信する)

さて、センサーも動かすことができたので、ここからが本番です。

環境光センサーの照度と、土壌水分センサーのアナログ値、デジタル値、土壌水分量をアドバタイズデータに載せて発信することにします。今回も弊社が規定し公表しているオープンセンササービスのビーコンフォーマットに則って、各センサー値を発信しようと思います。

今回もセンサーは照度と土壌水分アナログ値を「符号なし2バイトリスト」、土壌水分デジタル値と土壌水分量を「符号なし1バイトリスト」として扱うことにします。

もともと機能仕様としてプロンプト指示していましたので、機能追加部分を与えることにします。与えたプロンプトは以下のとおりです。

無事、M5CoreS3 SE、M5Stack用環境光ユニット(BH1750FVI搭載)、
M5Stack用土壌水分センサユニット(Unit Earth)で期待する動作を確認することができました。
仕様に、以下の機能を追加したいと思います。


機能
3.BLEアドバタイズ発信
・取得した環境光(lx)、土壌水分量(%)、アナログ値、デジタル値それぞれをアドバタイズデータに載せて発信する
・1秒毎に取得した各値でアドバタイズデータを更新する
・アドバタイズ周期は100msとする
・アドバタイズ種別はADV_NONCONN_INDとする
・送信出力(TxPower)は日本国内で設定可能な最大値とする
・AdvDataは2つのAD Structureで構成し、以下のとおりとする。

 1)AD Type:0x01(Flags)
 Flag:0x06を指定

 2)AD Type:0x16(Service Data - 16-bit UUID)
 16bit UUID:0xFCBE(リトルエンディアンで指定が必要な場合は注意すること)

 以降のデータは
 010000000030000152LLLL50MM52AAAA50DD(16進数)とする

 LLLL:環境光(16bit/ビッグエンディアン)
 MM:土壌水分量(8bit/ビッグエンディアン)
 AAAA:土壌水分アナログ値(16bit/ビッグエンディアン)
 DD:土壌水分デジタル値(8bit/ビッグエンディアン)

 をそれぞれ代入する

 これは、(株)ムセンコネクトが提唱しているオープンセンサーサービスを活用しています。
https://www.musen-connect.co.jp/blog/course/product/howto-16bituuid-ble-beacon-open-sensor-service/

 オープンセンサーサービスの仕様書です。
 必要に応じて確認してください。
 https://www.musen-connect.co.jp/dl/open_sensor_beacon_spec.pdf

一度コンパイルエラーが発生したため、エラーコードをプロンプトで与え修正してもらいました。エラー内容が前回記事のカラーセンサーで発生したエラーとまったく同じエラーでした。AIでも同じミスを犯すんだな、とすこし新鮮な気持ちになりました。

ご指摘の通り、以前のカラーセンサーのプロジェクトと全く同じ原因でコンパイルエラーが発生していますね。
BLEライブラリの関数が期待するString型と、コードが渡しているstd::string型が異なっているためです。

最終的に生成されたArduinoスケッチ(プログラムコード)は以下のとおりです。

/**
 * @file M5CoreS3_Sensor_Test.ino
 * @author Gemini
 * @brief M5CoreS3 SEで環境光・土壌水分センサの値を取得し、BLEアドバタイズするプログラム
 * @version 3.1
 * @date 2025-08-06
 * @copyright Copyright (c) 2025
 *
 * @note
 * v3.1: setServiceDataの引数の型が異なっていたことによるコンパイルエラーを修正。
 * v3.0: 取得したセンサーデータをBLEアドバタイズする機能を追加。
 * v2.2: 土壌水分センサーのキャリブレーション値更新。「Humidity」の表示を「Moisture」に修正。
 * v2.1: 土壌水分センサーのキャリブレーション値を暫定値に更新。
 * v2.0: M5Stack用土壌水分センサユニット(Unit Earth)の読み取り機能を追加。
 * v1.0: 環境光ユニット(BH1750FVI)の動作確認プログラム。
 *
 * ライブラリ:
 * "BH1750 by Christopher Laws" をインストールしてください。
 *
 * 機能:
 * 1. PortA, PortBのセンサーから1秒毎にデータを取得する。
 * 2. 取得した値をディスプレイに表示する。
 * 3. 取得した値をオープンセンサーサービスのフォーマットでBLEアドバタイズする。
 */


#include <M5Unified.h>
#include <Wire.h>
#include <BH1750.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEAdvertising.h>
#include <esp_bt.h> // 送信出力設定用


// --- センサーのインスタンスを作成 ---
BH1750 lightMeter;


// --- PortB (土壌水分センサ) のピン定義 ---
const int SOIL_ANALOG_PIN = 8;
const int SOIL_DIGITAL_PIN = 9;


// --- 土壌水分の換算のためのキャリブレーション値 ---
const int DRY_VALUE = 4095;
const int WET_VALUE = 2100;


// --- BLE関連の定義 ---
#define SERVICE_UUID_16BIT 0xFCBE
BLEAdvertising *pAdvertising = nullptr;


// 最後にセンサを読み取った時刻を保存する変数
unsigned long lastReadTime = 0;


/**
 * @brief BLEアドバタイズデータを更新する
 * @param lux 環境光 (lx)
 * @param moisture 土壌水分量 (%)
 * @param analog_val 土壌水分アナログ値
 * @param digital_val 土壌水分デジタル値
 */
void updateAdvertisement(float lux, int moisture, int analog_val, int digital_val) {
  if (pAdvertising == nullptr) return;


  pAdvertising->stop();


  BLEAdvertisementData oAdvertisementData = BLEAdvertisementData();
  oAdvertisementData.setFlags(0x06);


  // ペイロードを構築: 0100000000300001 52LLLL 50MM 52AAAA 50DD
  uint8_t payload[18];
  // ヘッダー部
  payload[0] = 0x01; // Data Schema Version
  payload[1] = 0x00; // Individual Identification Number
  payload[2] = 0x00;
  payload[3] = 0x00;
  payload[4] = 0x00;
  payload[5] = 0x30; // Data Type: Version
  payload[6] = 0x00; // Version Number (0x0001)
  payload[7] = 0x01;


  // データ部
  uint16_t lux_16bit = (uint16_t)lux;
  payload[8] = 0x52; // Sensor Data Type: Light
  payload[9] = (lux_16bit >> 8) & 0xFF; // LLLL (Big-Endian)
  payload[10] = lux_16bit & 0xFF;


  payload[11] = 0x50; // Sensor Data Type: Moisture
  payload[12] = (uint8_t)moisture; // MM


  uint16_t analog_16bit = (uint16_t)analog_val;
  payload[13] = 0x52; // Sensor Data Type: Analog
  payload[14] = (analog_16bit >> 8) & 0xFF; // AAAA (Big-Endian)
  payload[15] = analog_16bit & 0xFF;
 
  payload[16] = 0x50; // Sensor Data Type: Digital
  payload[17] = (uint8_t)digital_val; // DD


  // ★修正点: std::stringではなく、ArduinoのString型でデータを渡す
  oAdvertisementData.setServiceData(BLEUUID((uint16_t)SERVICE_UUID_16BIT), String((char*)payload, sizeof(payload)));


  pAdvertising->setAdvertisementData(oAdvertisementData);
  pAdvertising->start();
}


/**
 * @brief BLEの初期設定
 */
void setupBLE() {
  BLEDevice::init("M5-PlantSensor");
  esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, ESP_PWR_LVL_P9); // 送信出力を最大(+9dBm)に設定
  pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->setAdvertisementType(ADV_TYPE_NONCONN_IND);
  pAdvertising->setMinInterval(160); // 100ms
  pAdvertising->setMaxInterval(160); // 100ms
  updateAdvertisement(0, 0, 0, 0); // 初回アドバタイズ
  Serial.println("BLE Advertising started.");
}


/**
 * @brief 初期設定
 */
void setup() {
  M5.begin();
  Serial.begin(115200);


  // PortA (環境光センサ)
  Wire.begin(2, 1);
  M5.Display.setTextSize(2);
  M5.Display.println("Sensor Test");


  if (lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE, 0x23, &Wire)) {
    M5.Display.println("- Found BH1750 sensor");
  } else {
    M5.Display.println("- BH1750 not found...");
  }


  // PortB (土壌水分センサ)
  pinMode(SOIL_ANALOG_PIN, INPUT);
  pinMode(SOIL_DIGITAL_PIN, INPUT);
  M5.Display.println("- Earth Unit setup done");


  delay(1500);
  M5.Display.fillScreen(BLACK);


  // BLEの初期設定
  setupBLE();
}


/**
 * @brief メインループ
 */
void loop() {
  if (millis() - lastReadTime > 1000) {
    lastReadTime = millis();


    // --- 1. センサーデータ取得 ---
    float lux = -1.0;
    if (lightMeter.measurementReady()) {
      lux = lightMeter.readLightLevel();
    }


    int soil_analog = analogRead(SOIL_ANALOG_PIN);
    int soil_digital = digitalRead(SOIL_DIGITAL_PIN);
    int moisture = constrain(map(soil_analog, DRY_VALUE, WET_VALUE, 0, 100), 0, 100);


    // --- 3. BLEアドバタイズ発信 ---
    updateAdvertisement(lux, moisture, soil_analog, soil_digital);




    // --- 2. ディスプレイ表示 ---
    M5.Display.fillScreen(BLACK);
    M5.Display.setCursor(10, 10);
    M5.Display.setTextSize(2);
    M5.Display.setTextColor(WHITE);


    M5.Display.println("Ambient Light:");
    M5.Display.setCursor(30, 40);
    M5.Display.setTextColor(YELLOW);
    M5.Display.printf("%.2f lx\n\n", lux);


    M5.Display.setTextColor(WHITE);
    M5.Display.setCursor(10, 100);
    M5.Display.println("Soil Moisture:");
   
    M5.Display.setTextColor(CYAN);
    M5.Display.setCursor(30, 130);
    M5.Display.printf("Analog : %d\n", soil_analog);
   
    M5.Display.setCursor(30, 160);
    M5.Display.printf("Digital: %s\n", (soil_digital == 0) ? "WET" : "DRY");


    M5.Display.setCursor(30, 190);
    M5.Display.printf("Moisture: %d %%\n", moisture);


    // シリアルモニタにも出力
    Serial.printf("Light: %.2f lx, Soil Analog: %d, Moisture: %d%%\n",
                  lux, soil_analog, moisture);
  }
  M5.update();
}

プロンプト指示(M5Core Grayをオブザーバーにする)

では、もう1台あるM5Core Grayをオブザーバーにします。M5CoreS3 SEのアドバタイズデータをキャッチして、屋外の家庭菜園のデータを室内から確認できるようにします。水分が足りていないようであれば、外に出て水やりができるようにします!

では、さらに手元にあるM5Core Grayを用いて、M5CoreS3 SEがアドバタイズ発信している内容を受信し、M5CoreS3 SEのディスプレイ表示と同じような表示を行いたい。

仕様を以下に示す。 
仕様を満たすプログラムコードを提示してほしい。

開発環境 
 ・Arduino IDE 

 目的 
 ・先ほどのM5CoreS3 SEが発信しているアドバタイズデータを受信し、自身のディスプレイ表示に反映させる

機能 
 1.アドバタイズデータ取得 
 ・Bluetooth LEのアドバタイズをスキャンする 
 ・アドバタイズデータを以下の条件でフィルタリングする

 (株)ムセンコネクトが提唱しているオープンセンサーサービスを利用していること
 https://www.musen-connect.co.jp/blog/course/product/howto-16bituuid-ble-beacon-open-sensor-service/

 オープンセンサーサービスの仕様書です。
 必要に応じて確認してください。
 https://www.musen-connect.co.jp/dl/open_sensor_beacon_spec.pdf

 AD Type:0x16(Service Data - 16-bit UUID)
 16bit UUID:0xFCBE(リトルエンディアンで指定が必要な場合は注意すること)
 オープンセンサーサービスのヘッダー部の個体識別番号が0x00000000であること
 環境光センサーの照度値(データ種別0x52)、土壌水分センサーの水分量(データ種別0x50)、アナログ値(データ種別0x52)、デジタル値(データ種別0x50)と続き、ペイロードが18byteであること


 2.ディスプレイ表示 
 ・M5CoreS3 SEと同じ表示を行う
 ・土壌水分量が20%を下回った時のみ、ディスプレイの背景を赤くする

また、一度コンパイルエラーが発生したため、エラーコードを貼り付けて修正してもらいました。そろそろエラー無く生成されてほしいところです。最終的に生成されたArduinoスケッチ(プログラムコード)は以下のとおりです。

/**
 * @file M5Core_Gray_Plant_Monitor.ino
 * @author Gemini
 * @brief M5CoreS3 SEが発信する植物センサー情報を受信・表示するプログラム
 * @version 1.1
 * @date 2025-08-06
 * @copyright Copyright (c) 2025
 *
 * @note
 * v1.1: BLEライブラリのバージョン差異によるコンパイルエラーを修正。
 * v1.0: M5CoreS3 SEからのオープンセンサーサービスのアドバタイズを受信し、
 * 環境光と土壌水分の状態を表示する。
 * 土壌水分量が20%未満になると背景を赤くして警告する。
 * 安定動作のため、メインループ内で同期スキャンを実行する方式を採用。
 *
 * ライブラリ:
 * "M5Unified" をインストールしてください。
 *
 * 機能:
 * 1. オープンセンサーサービス(UUID:0xFCBE)のアドバタイズをスキャンする。
 * 2. 指定された条件でデータをフィルタリングする。
 * 3. 受信したセンサー値をM5CoreS3 SEと同様のレイアウトで表示する。
 * 4. 土壌水分量が20%未満になるとディスプレイ背景を赤色にする。
 */


#include <M5Unified.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>


// --- スキャン関連の定義 ---
#define SERVICE_UUID_16BIT 0xFCBE
#define SCAN_TIME_SECONDS 1 // 1秒ごとにスキャン


BLEScan* pBLEScan;


/**
 * @brief 初期設定
 */
void setup() {
    M5.begin();
    Serial.begin(115200);


    M5.Display.setTextSize(2);
    M5.Display.println("Plant Monitor");


    // BLEの初期化
    BLEDevice::init("");
    pBLEScan = BLEDevice::getScan();
    pBLEScan->setActiveScan(true);
    pBLEScan->setInterval(100);
    pBLEScan->setWindow(99);


    Serial.println("Setup complete. Starting scan loop...");
    M5.Display.println("Scanning for sensor...");
    delay(1000);
}


/**
 * @brief メインループ
 */
void loop() {
    // 指定した時間だけ同期的にスキャンを実行し、結果を取得する
    BLEScanResults* foundDevices = pBLEScan->start(SCAN_TIME_SECONDS, false);


    bool deviceFoundAndProcessed = false;


    // 見つかったデバイスを一つずつチェック
    for (int i = 0; i < foundDevices->getCount(); i++) {
        BLEAdvertisedDevice advertisedDevice = foundDevices->getDevice(i);


        // 1. Service Data (UUID: 0xFCBE) が含まれているか確認
        if (advertisedDevice.haveServiceData() && advertisedDevice.getServiceDataUUID().equals(BLEUUID((uint16_t)SERVICE_UUID_16BIT))) {
            
            String serviceData = advertisedDevice.getServiceData();


            // 2. ペイロード長が18バイトであるか確認
            if (serviceData.length() != 18) continue;


            const uint8_t* payload = (const uint8_t*)serviceData.c_str();


            // 3. 個体識別番号が0x00000000であるか確認
            if (payload[1] != 0x00 || payload[2] != 0x00 || payload[3] != 0x00 || payload[4] != 0x00) continue;


            // 4. データ種別が正しい順序であるか確認
            if (payload[8] != 0x52 || payload[11] != 0x50 || payload[13] != 0x52 || payload[16] != 0x50) continue;


            // --- 全てのフィルタリング条件をクリア ---
            
            // データを抽出
            uint16_t lux = (payload[9] << 8) | payload[10];
            uint8_t moisture = payload[12];
            uint16_t soil_analog = (payload[14] << 8) | payload[15];
            uint8_t soil_digital = payload[17];


            // -----------------------------------------------------
            // ディスプレイ表示処理
            // -----------------------------------------------------
            // 土壌水分量が20%未満なら背景を赤に、そうでなければ黒にする
            if (moisture < 20) {
                M5.Display.fillScreen(RED);
            } else {
                M5.Display.fillScreen(BLACK);
            }
            
            M5.Display.setCursor(10, 10);
            M5.Display.setTextSize(2);
            M5.Display.setTextColor(WHITE);


            M5.Display.println("Ambient Light:");
            M5.Display.setCursor(30, 40);
            M5.Display.setTextColor(YELLOW);
            M5.Display.printf("%u lx\n\n", lux);


            M5.Display.setTextColor(WHITE);
            M5.Display.setCursor(10, 100);
            M5.Display.println("Soil Moisture:");
            
            M5.Display.setTextColor(CYAN);
            M5.Display.setCursor(30, 130);
            M5.Display.printf("Analog : %u\n", soil_analog);
            
            M5.Display.setCursor(30, 160);
            M5.Display.printf("Digital: %s\n", (soil_digital == 0) ? "WET" : "DRY");


            M5.Display.setCursor(30, 190);
            M5.Display.printf("Moisture: %u %%\n", moisture);


            deviceFoundAndProcessed = true;
            // 最初の1つだけ処理してループを抜ける
            break; 
        }
    }


    // スキャン結果をクリアして次のスキャンに備える
    pBLEScan->clearResults();


    // M5Unifiedではボタン状態の更新などに必要
    M5.update();
}

動作を確認する

M5Core Grayにも書き込みができましたので、実際に設置をして動作を確認していきます。最近会社の従業員出入口でミニトマトの栽培を(勝手に)始めたので、そちらにM5CoreS3 SEと各センサーを取り付けてデータ収集をしたいと思います。

従業員入口のミニトマト
太陽光の測定

やはり室内とは違い、環境光センサーの照度が段違いですね。撮影した日は曇天だったのですが、それでも30,000ルクスを超えています。

さて、あとは室内にM5Core Grayを配置して、遠隔でミニトマトの様子を確認できるようにしておきます。ミニトマトモニターと仮に名前を付けたいと思います。当初の予定通り、水やりのタイミングみたいなものが、室内からわかるとよさそうです。

弊社サービスフロア

設置して数日経過したところでミニトマトモニターが赤く点滅していました!土壌水分量が暫定しきい値の30%を下回ったようです。おかげで水やりをすることができました。

これでこまめに外に確認しにいかなくてもよさそうですし、継続したデータ収集もできそうです。

実際にセンサーをつかってみてわかったこと

設置後、しばらくすると土壌水分量(Moisture)が常に0%を示すようになりました。水やりをした後も変化がみられなかったため、センサーの故障を疑い、一度とりはずして確認することにしました。

センサーの回収
土壌水分センサーのパッド剥がれ

土壌水分センサーのパッドが剥がれてしまっています。土壌成分、pH、あるいはバクテリアの影響かわかりませんが、このセンサーは元々パッド間の抵抗値から値を算出する仕組みのため、パッドが剥がれてしまうと正確な値が得られなくなります。

正直なところ、これは想定外でした。土に直接挿して使うものなのに、このようにパッドが剥がれてしまうのであれば、用途として適さないと感じます。やはり理論だけでなく、実際にやってみないとこのようなリアルな失敗や気付きが得られないので、試してみることが大事であると再認識しました。

まとめ

想定していた「スマート農業のような家庭菜園のIoT化」の手順として、Bluetooth LEのアドバタイズデータを使った具体例を示すことができました。継続的な動作を目指していましたが、土壌水分センサーのパット剥がれにより断念せざるを得なくなりました。しかし、この失敗は必ず次のアイデアにつながることと思います。

今回も、ムセンコネクトが提唱する「オープンセンササービス ビーコン仕様」を活用することで、気軽にアドバタイズデータの発信ができました。

繰り返しになりますが、Bluetoothの通信は用途によりプロファイルやサービス、キャラクタリスティックが規定されているものが多いですが、アドバタイズデータはセンサー値などの規定がありません。我々の考えや、公開の経緯はこちらの記事に載っていますのでぜひ併せてご覧ください。

冒頭にもお伝えしましたが、M5Stack自体はBluetooth認証を取得していないため、M5Stackで製品を開発しようとすると、最後のBluetooth認証取得の際に困難が待ち構えています。M5Stackを用いた製品化をご検討の際は、この点に十分ご注意ください。

次回は、M5Stackとスマートフォン間のBluetooth LEペアリングにチャレンジしたいと思います。ペアリングをすると、お互いが通信相手を覚え、通信データを暗号化することができます。ペアリングについてはこちらの記事に載っていますのでぜひ併せてご覧ください。

お楽しみに!

よろしければシェアをお願いします
目次