見出し画像

温湿度計ゲートウェイ(THGateway)の作製【完成版】

はじめに

前回、完成目前で全く先が見えなくなってしまった温湿度計ゲートウェイの作製です。

温湿度計ゲートウェイ(THGateway)の作製(途中)

問題点の整理から始めます。

 ・M5AtomMatrixにおいて、データの受信が安定していない。

 ・M5Stackで正しくデータが取れていない。

 ・なんだか色が変。

赤色には設定した覚えないけどなぁ。

ここまでの事象を元に思ったことは、
 ・M5AtomMatrixで電波の受信は問題ないか?
 ・M5Stackの事象から得られることが何かあるんじゃないか?
 ・開発環境やライブラリをアップデートしたので、何か変わってしまったことがあるのではないか?
ということです。

一回きれいに忘れた状態から思い出しながら再プログラムすることにしました。

紆余曲折があって、現在のプログラムにたどり着いているとは思うのですが、もう一度トレースしてみる必要がありそうです。

仕事で取り組む場合、まずは現在起きていることの原因究明から始めます。しかし今までこの原因究明でさらに深い沼に沈んでしまったこともあるため、今回は今あるものをそっと閉じ、仕切り直すことを選びました。

ネットで得られる情報、スマホのアプリで得られる情報、M5Stackで得られる情報との整合性がいまひとつなところも感じています。

そもそも、M5StackやM5AtomMatrixでBLEが利用できるのは、ESP32を搭載しているためです。ということは、Arduino IDEのESP32サンプルをもう一度きちんと読んでみようと思いました。

ArduinoIDEで表示したサンプル

Arduinoのサイトからドキュメントがたどれるようになっていました。

Arduino Reference > Libraries > ESP32 BLE Arduino

https://www.arduino.cc/reference/en/libraries/esp32-ble-arduino/

ドキュメント

英語はそれほど得意ではないので、PDFを一度DeepL翻訳に通して、和文を読んだ後、本文を読みます。

そして、用意されているサンプルに目を通していきます。

ESP32 BLE関連サンプルのまとめ(自分用)

以前も一度目を通したことがあるのを思い出しました。その時は事前にドキュメントを読んでいなかったので、クラス定義しコールバック関数をオーバーライドするのを見て、できればこれはやりたくないなと感じ、コールバック関数を定義せず、スキャン後のデバイスの個数をカウントして、データを取っていました。

こんな感じです。

    :(略)
void loop()
{
    :(略)
  BLEScanResults foundDevices = pBLEScan->start(1, false);
  uint32_t dev_count = foundDevices.getCount(); // 受信したデバイス数を取得
    :(略)
  for (int i = 0; i < dev_count; i++) {
    BLEAdvertisedDevice device = foundDevices.getDevice(i);
    BLEAddress address = device.getAddress();
    :(略)
  }
  pBLEScan->clearResults(); // delete results fromBLEScan buffer to release memory
    :(略)
}

これで間違っていない気もするのですが、実際には動作していません。簡単に調べてみただけですが、foundDevices.getCount()が0で返ることが多く、空回りしている感じでした。

上記のサンプルから、今回やりたいことに一番近い「BLE_scan」をベースに改造を加えていくことにしました。

    :(略)
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
      Serial.printf("Advertised Device: %s \n", advertisedDevice.toString().c_str());
    }
};
    :(略)
void setup() {
    :(略)
  BLEDevice::init("");
  pBLEScan = BLEDevice::getScan(); //create new scan
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setActiveScan(true); //active scan uses more power, but get results faster
  pBLEScan->setInterval(100);
  pBLEScan->setWindow(99);  // less or equal setInterval value
    :(略)
}

void loop() {
  // put your main code here, to run repeatedly:
  BLEScanResults foundDevices = pBLEScan->start(scanTime, false);
  Serial.print("Devices found: ");
  Serial.println(foundDevices.getCount());
  Serial.println("Scan done!");
  pBLEScan->clearResults();   // delete results fromBLEScan buffer to release memory
  delay(2000);
}

BLE_scanの一部

腹を据えてソースを見てみると、クラスのオーバーロードと言っても、ひとつのコールバック関数(onResult())を上書きしているだけで、それほど難しくはなさそうです。

M5AtomMatrix用に必要な処理を行い、サンプルプログラムを走らせてみました。
今までは、BLEのスキャンを一定時間行い、終了後個数を調べて順番に見つかったデバイスの情報に当たっていました。

今回はコールバック関数(onResult())の中で必要な処理を行う形に変更しました。
スキャンする時間や休む時間を色々変化させて試しましたが、時間によってはデータがうまく取れないこともあり、このあたりの難しさを感じました。

結局サンプルプログラムの通り、5秒間スキャンを行い、2秒待機することにしました。

せっかくコールバック関数を使ったので、様子を見える化してみました。

    //activeサイン
    static int ff = 1;
    M5.dis.drawpix(4, 4, (ff ^= 1) ? RGB(0x40, 0x00, 0x00) : 0);

受信するたびに右下のLEDをチカチカするようにしています。

お気づきになられたでしょうか?
若干のデザイン変更と色を変更しています。

新デザインは

1-★5-
-2★-6
3-★7-
-4★-8
〇〇〇〇▲

1:アドレス1
2:アドレス2
3:アドレス3
4:アドレス4
5:アドレス5
6:アドレス6
7:アドレス7
8:アドレス8
★:Ambient更新までの残り時間
〇:fConnect
▲:BLE信号キャッチ毎に明滅

となりました。
ご注目頂きたい点は〇印が赤から緑に変わった点です。

元々私はコードの中で

  //Wifiの接続状況
  int c = (fConnect) ? RGB(0x00, 0x80, 0x00) : 0;
  for (int x = 0; x < 5; x++)
    M5.dis.drawpix(x, 4, c);

と書いていました。(旧ソースのため、下一列全て緑に表示するつもりでした。)

ところが、実際には、ん?となった

の表示。すぐにピンときました。

ライブラリの仕様が変わったな。。。

#define  RGB(r, g, b)  (((g) << 16) + ((r) << 8) + (b))

rとgが反転したような作りだったので、エレガントではないと思っていたのですが、ライブラリの仕様が変わったとしか思えないような変更ですね。

#define  RGB(r, g, b)  (((r) << 16) + ((g) << 8) + (b))

このように変更することで、意図通りの色で表示できるようになりました。仕事でプログラミングを行うときは、このようなことが起こる可能性があるため、安易にライブラリをアップデートしたりしませんが、趣味のプログラミングでは、このようなことも含めて楽しいと思っています。(泥沼にはまらなければ。)

また、温湿度計から得られるデータの温度だけに絞り、同時に最大8個のデータロガーからの情報を受け付けることにしました。

この状態で一晩様子を見て、安定してデータが取れることを確認したので、次のステップへ進むことにしました。

次の目標はCYALKIT-E03のデータを取得することです。

前回記事でも触れましたが、ずいぶん昔、まだ電子工作に興味を持つ前に、棚場の温度管理のために何かできないかと考えていた時、Cypress Semiconductor社(現在はInfineon)のCYALKIT-E03を入手していました。

そのころは、マイコンの存在も知らず、スマホのアプリをちょこちょこっと作れば、欲しいデータがさっと手に入るだろうと、ずいぶんお気楽に考えておりました。

Cypress BLE-Beaconというアプリを用いてモニタリングはできるのですが、アプリを動かしている間だけなので、「データロガー」としては使えません。

この代物をなんとか使いこなしたいとかねてより考えておりました。

昔つぶやいた痕跡を発見!このころからミニ百葉箱が作りたかったんだ!

スマホのアプリCypress BLE-Beaconを動かし、個体の識別をします。

この内容に合致するデータがキャッチできるか、シリアルモニタに表示されるデータを追っかけます。

Advertised Device: Name: , Address: XX:XX:XX:XX:XX:XX, manufacturer data: 4c0002150005000100001000800000805f9b01310003486fc3

これですね。UUIDが一致しています。Manufacturer dataを取り出し前回作った構造体で取り出せば、きれいに読めるはずです。

  // CYALKIT温湿度計を特定する。
    if (advertisedDevice.haveManufacturerData() == true
        && advertisedDevice.getManufacturerData().length() == CYALKIT_MD_SIZE) {
      CyalkitManufacturerData md;
      memcpy(&md, advertisedDevice.getManufacturerData().data(),
          advertisedDevice.getManufacturerData().length());
 
      if (md.companyID == 0x004C && md.deviceType == 0x02) {
        float temperature = (float)(175.72 * (md.temp * 256.0) / 65536.0 - 46.85);
        float humidity = (float)(125.0 * (md.humidity * 256.0) / 65536.0 - 6.0);

このように処理しました。

数日間、試した結果が次の通りです。

同時に8台のデータロガーからデータがどんどん飛んでくる様を予想していたので、結果はちょっと予想外のものでした。

今回作製した、温湿度計ゲートウェイはBLEとWifi二つの通信を行います。
また、前回まではM5Stackを用いて行っていた処理をM5AtomMatrixに変更しています。

ちょっと調べただけではわからなかったので、次回以降の課題となりますが、肌感覚では、M5AtomMatrixのBLE受信能力がちょっと弱い感じがしています。

点で見えるデータは、CYALKIT-E03のものです。
TMモードで動かしています。

ドキュメントには、

DM:デモモード 3~60秒間隔でデータを送信する。(充電しないで)
TM:タイマーモード 充電しながら5分間隔でデータを送信する。
(タイマーモードでフル充電時、環境光なしで30時間以上データ送信が可能)

とあります。

あちこちに散逸していたCYALKIT-E03を探しながら、配置していきました。ひとつは屋外に。


残る4つはバルコニーサッシの屋内側に並べて。

その結果が上記のデータになります。
昼間は日照があるので、充電しやすく、データの発信も行えているようですが、夜はデータが飛びがちですね。
信号がほとんどキャッチできないものもあります。

黄色の線は寝室に置いた温湿度計なのですが、昼間はデータが飛びがちになっています。
多分ですが、人がいたほうがBLE電波が乱反射して飛びやすくなっているのではないかと推測しています。

こんな結果にはなりましたが、3年以上眠らせたデバイスを蘇らせたということに大いに感動。
フラグを回収することができました。

ソースはこちらです。

// THGatewayAM bbd
// AtomMatrix用温湿度計ゲートウェイ
// ArduinoIDEスケッチ例ESP32 BLE ArduinoのBLE_scanを参考に
//
//近くの温湿度計から発せられるBLE信号をキャッチし、Ambientへデータを送ります。

#include <M5Atom.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>
#include "Ambient.h"

#define  RGB(r, g, b)  (((r) << 16) + ((g) << 8) + (b))

#define SECONDS(s)    ((s) * 1000)
#define MINUTES(m)    SECONDS(m * 60)

#define SWITCHBOT_SD_SIZE 6
#define CYALKIT_MD_SIZE 25

typedef struct {
  // Byte:0 Enc Type Dev Type
  uint8_t deviceType : 7;
  uint8_t reserved1 : 1;
  // Byte:1 Status
  uint8_t groupA : 1;
  uint8_t groupB : 1;
  uint8_t groupC : 1;
  uint8_t groupD : 1;
  uint8_t reserved2 : 4;
  // Byte:2 Update UTC Flag Battery
  uint8_t remainingBattery : 7;
  uint8_t reserved3 : 1;
  // Byte:3
  uint8_t decimalOfTheTemperature : 4;
  uint8_t humidityAlertStatus : 2;
  uint8_t temperatureAlertStatus : 2;
  // Byte:4
  uint8_t integerOfTheTemperature : 7;
  uint8_t posiNegaTemperatureFlag : 1;
  // Byte:5
  uint8_t humidityValue : 7;
  uint8_t temperatureScale : 1;
} SwitchBotServiceData;

typedef struct {
  // AD Data 25 bytes
  uint16_t  companyID;  // 0x004C
  uint8_t   deviceType; // 0x02
  uint8_t   length3;    // 0x15
  char      uuid[16];   // 00050001-0000-1000-8000-00805F9B0131 [hex]
  uint16_t  major;      // 0x0001
  uint8_t   humidity;   // 湿度[%] = 125 * (humidity * 256) / 65536 - 6
  uint8_t   temp;       // 温度[℃] = 175.72 * (temp * 256) / 65536 - 46.85
  uint8_t   rssi;       // RSSI
} CyalkitManufacturerData;

int scanTime = 5; //In seconds
int waitTime = 2; //In seconds
int intervalTime = 20 * 60; //In seconds
int skipResetCount = intervalTime / (scanTime + waitTime);
int skipCount = skipResetCount;

BLEScan* pBLEScan;

#define MAX_ADDRESS 8

int nAddresses = 0;
std::string addresses[MAX_ADDRESS];


boolean fConnect = false;
WiFiClient client;
// Wi-Fi
const char * ssid     = "XXXX";
const char * password = "XXXX";

Ambient ambient;
unsigned int channelId = 0; // AmbientのチャネルID
const char* writeKey = "XXXX"; // ライトキー

void
printAddressPos(void)
{
  Serial.printf("***** printAddressPos(%d) *****\n", nAddresses);
  int loopCount = (nAddresses < MAX_ADDRESS) ? nAddresses : MAX_ADDRESS;
  for (int i = 0; i < loopCount; i++)
    Serial.printf("address[%d]:%s\n", i, addresses[i]);
  Serial.printf("*****\n");
}

int getAddressPos(const std::string tAddress)
{
  // 見つけたMACアドレス毎に番号を振り、その番号でAmbientへデータを書き込む。
  // 最大数を超えた場合、-1を返す。
  bool fFound = false;
  int pos = -1;
  int loopCount = (nAddresses < MAX_ADDRESS) ? nAddresses : MAX_ADDRESS;
  for (int i = 0; i < loopCount; i++) {
    if (tAddress == addresses[i]) {
      fFound = true;
      pos = i;
      continue;
    }
  }
  if ((nAddresses < MAX_ADDRESS) && !fFound) {
    pos = nAddresses;
    addresses[nAddresses++] = tAddress;
  }
  return pos;
}

class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    //activeサイン
    static int ff = 1;
    M5.dis.drawpix(4, 4, (ff ^= 1) ? RGB(0x40, 0x00, 0x00) : 0);

    Serial.printf("Advertised Device: %s \n", advertisedDevice.toString().c_str());

    BLEAddress address = advertisedDevice.getAddress();

    if (advertisedDevice.haveServiceData()) {
      std::string d = advertisedDevice.getServiceData();
      Serial.printf("ServiceData(%d):", d.size());
      for (int i = 0; i < d.size(); i++) {
        Serial.printf("%02X", d[i]);
      }
      Serial.print("\n");
    }

    // Switchbot温湿度計を特定する。
    if (advertisedDevice.haveServiceData()
        && advertisedDevice.getServiceData().length() == SWITCHBOT_SD_SIZE) {
      SwitchBotServiceData sd;
      memcpy(&sd, advertisedDevice.getServiceData().data(),
          advertisedDevice.getServiceData().length());

      if (sd.deviceType == 'T') { // SwitchBot MeterTH (WoSensorTH) Normal Mode

        float temperature = ((sd.posiNegaTemperatureFlag) ? 1.0 : -1.0) *
                            (sd.decimalOfTheTemperature * 0.1 +
                             sd.integerOfTheTemperature);
        float humidity = (float)sd.humidityValue;

        Serial.printf("temperature:%.1f\n", temperature);
        Serial.printf("humidity:%.1f\n", humidity);

        //温度のみ送信する。
        if (fConnect) {
          int pos = getAddressPos(address.toString());
          if (pos >= 0) {
            ambient.set(pos + 1, temperature);
            Serial.printf("ambient.set(%d, %.1f);\n", pos + 1, temperature);
          }
        }

        Serial.printf("SwitchBotServiceData:\n");
        Serial.printf("   deviceType:%02x\n", sd.deviceType);
        Serial.printf("   reserved1:%02x\n", sd.reserved1);
        Serial.printf("   group A:%02x B:%02x C:%02x D:%02x\n", sd.groupA, sd.groupB, sd.groupC, sd.groupD);
        Serial.printf("   reserved2:%02x\n", sd.reserved2);
        Serial.printf("   remainingBattery:%02x(%d)\n", sd.remainingBattery, sd.remainingBattery);
        Serial.printf("   reserved3:%02x\n", sd.reserved3);
        Serial.printf("   decimalOfTheTemperature:%02x(%d)\n", sd.decimalOfTheTemperature, sd.decimalOfTheTemperature);
        Serial.printf("   humidityAlertStatus:%02x\n", sd.humidityAlertStatus);
        Serial.printf("   temperatureAlertStatus:%02x\n", sd.temperatureAlertStatus);
        Serial.printf("   integerOfTheTemperature:%02x(%d)\n", sd.integerOfTheTemperature, sd.integerOfTheTemperature);
        Serial.printf("   posiNegaTemperatureFlag:%02x\n", sd.posiNegaTemperatureFlag);
        Serial.printf("   humidityValue:%02x(%d)\n", sd.humidityValue, sd.humidityValue);
        Serial.printf("   temperatureScale:%02x\n", sd.temperatureScale);
      }
    }

    // CYALKIT温湿度計を特定する。
    if (advertisedDevice.haveManufacturerData() == true
        && advertisedDevice.getManufacturerData().length() == CYALKIT_MD_SIZE) {
      CyalkitManufacturerData md;
      memcpy(&md, advertisedDevice.getManufacturerData().data(),
          advertisedDevice.getManufacturerData().length());

      if (md.companyID == 0x004C && md.deviceType == 0x02) {
        float temperature = (float)(175.72 * (md.temp * 256.0) / 65536.0 - 46.85);
        float humidity = (float)(125.0 * (md.humidity * 256.0) / 65536.0 - 6.0);

        Serial.printf("temperature:%.1f\n", temperature);
        Serial.printf("humidity:%.1f\n", humidity);

        //温度のみ送信する。
        if (fConnect) {
          int pos = getAddressPos(address.toString());
          if (pos >= 0) {
            ambient.set(pos + 1, temperature);
            Serial.printf("ambient.set(%d, %.1f);\n", pos + 1, temperature);
          }
        }

        Serial.printf("CyalkitManufacturerData:\n");
        Serial.printf("   companyID:%04x\n", md.companyID);
        Serial.printf("   deviceType:%02x\n", md.deviceType);
        Serial.printf("   length3:%02x\n", md.length3);
        Serial.printf("   uuid:%s\n", md.uuid);
        Serial.printf("   humidity:%02x\n", md.humidity);
        Serial.printf("   temp:%02x\n", md.temp);
        Serial.printf("   rssi:%02x\n", md.rssi);
      }
    }
  }
};

void dispLCD(int skipCount)
{
  //Wifiの接続状況
  int c = (fConnect) ? RGB(0x00, 0x80, 0x00) : 0;
  for (int x = 0; x < 4; x++)
    M5.dis.drawpix(x, 4, c);

  //アドレス登録の状態
  c = RGB(0x20, 0x20, 0x20);
  for (int i = 0; i < nAddresses; i++)
    M5.dis.drawpix((i > 4) * 3 + (i % 2), (i % 4), c);

  //更新までの残り時間
  M5.dis.drawpix(2, 0, (skipCount > skipResetCount / 4 * 3) ? RGB(0x80, 0x80, 0x00) : 0);
  M5.dis.drawpix(2, 1, (skipCount > skipResetCount / 4 * 2) ? RGB(0x80, 0x80, 0x00) : 0);
  M5.dis.drawpix(2, 2, (skipCount > skipResetCount / 4) ? RGB(0x80, 0x80, 0x00) : 0);
  M5.dis.drawpix(2, 3, (skipCount > 0) ? RGB(0x80, 0x80, 0x00) : 0);
}

void setup() {
  M5.begin(true, false, true);
  Serial.begin(115200);
  Serial.println("Scanning...");

  BLEDevice::init("M5Atom");
  pBLEScan = BLEDevice::getScan(); //create new scan
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setActiveScan(true); //active scan uses more power, but get results faster
  pBLEScan->setInterval(100);
  pBLEScan->setWindow(99);  // less or equal setInterval value

  WiFi.begin(ssid, password);  //  Wi-Fi APに接続
  for (int i = 0; i < 30 && WiFi.status() != WL_CONNECTED; i++) {  //  Wi-Fi AP接続待ち
    Serial.print(".");
    delay(100);
  }
  Serial.print("\n");
  fConnect = (WiFi.status() == WL_CONNECTED);

  if (fConnect) {
    // チャネルIDとライトキーを指定してAmbientの初期化
    ambient.begin(channelId, writeKey, &client);
  }

  delay(SECONDS(1));
}

void loop() {
  dispLCD(skipCount);
  if (--skipCount < 0) {
    skipCount = skipResetCount;
    if (fConnect)
      ambient.send();
  }

  BLEScanResults foundDevices = pBLEScan->start(scanTime, false);
  Serial.print("Devices found: ");
  Serial.println(foundDevices.getCount());
  Serial.println("Scan done!");
  pBLEScan->clearResults();   // delete results fromBLEScan buffer to release memory

  printAddressPos();

  delay(SECONDS(waitTime));
}

余談

ゲートウェイで思い出しました。
以前私は熱烈なGateway2000ファンであったことを。

好きすぎて、当時ハマっていたレーシングカートを牛柄にし、工作上手の友人に作ってもらったステッカーを貼り付けていました。

牛号の雄姿。フロントカウルのステッカーは牛柄箱の前のロゴ

懐かしいです。

最後までお付き合いいただきまして誠にありがとうございます。
この状態でしばらく様子を見ることにいたします。

#IoT #温湿度計ゲートウェイ #Ambient #Switchbot温湿度計 #M5Stack #AtomMatrix #CYALKIT #Gateway2000

この記事が参加している募集

週末プロジェクト

この記事が気に入ったらサポートをしてみませんか?