重要なお知らせ

2024年10月04日 弊社製品の無線通信機能に関するお詫びとお願い

T&D ラボ

マイコンボードで取得した「おんどとり」の測定値をブラウザで見る

  • TR4Aシリーズ
  • おんどとりease
  • M5Stack

更新日2023.10.26 Thu
公開日2023.10.26 Thu

1.はじめに

T&DのTR4Aシリーズや、「おんどとりease (イーズ) 」ことTR32Bは、置かれた場所の温度や湿度を測定して記録する製品で、Bluetooth通信に対応しています。
本体の液晶画面 (LCD) はもちろん、スマート端末でも弊社の無料クラウドでも温湿度の値が確認できる、非常に便利な機種です。

今回は、ローカルLAN環境のPC上で、測定値(直近1分以内)をブラウザからほぼリアルタイムに確認する方法をご紹介します。

具体的には、TR4AシリーズやTR32B(以下、TR4Aと記載)の測定値が含まれるBluetooth Low Energyの電波をM5StickC Plus (以下、M5Stickと記載) というマイコンボードで受信し、M5Stickにブラウザからアクセスすると下記のように測定値が表示されるというものです。

今回のプロジェクトではローカルLAN環境内からのアクセスに限定されるものの、ブラウザからTR4Aの測定値が見られるようになり、下記のような応用が考えられます。

  • ・別室や冷蔵庫などBluetoothの電波が届かない場所の温度・湿度をPCやスマホから監視する。
  • ・複数のTR4Aの測定値や警報状態などを大型モニタに表示する。

アイディア次第でいくらでも可能性が広がりそうですね。
それでは、どうやって実現するかを以下で説明していきましょう。

2.動作概要

2-1. Bluetooth Low Energy通信

Bluetooth Low Energyでは、TR4Aのようなセンサ側はペリフェラルデバイス、ペリフェラルデバイスからデータを取得する側をセントラルデバイスと呼びます。スマホなどはセントラルデバイスの代表例ですが、今回はM5Stickを使ってセントラルデバイスを作成します。

ペリフェラルデバイスは、[図1]のように常時自らの情報を載せたアドバタイジングパケットという信号を送信しています。
TR4Aのアドバタイジングパケットには温度・湿度の測定データが含まれており、M5Stickはアドバタイジングパケットを受信し、その中の測定値を取得します。

図1:アドバタイジングパケットの送信イメージ

2-2. Webサーバ

M5StickはWebサーバとして動作し、PCやスマホなどのブラウザからアクセスするとTR4Aから受信した測定値を表示します。
HTTPのリクエストに対しレスポンスを返す動作を行います。動作概要を[図2]に示しました。

図2:Webサーバ

M5Stickは、起動後から下記の手順で動作します。

  • 1.M5Stickと無線LANのアクセスポイントが接続される。
  • 2.M5StickのIPアドレスがDHCPサーバから割り当てられる。
  • 3.M5Stickのドメイン名がmDNSにより「wssnano.local」と設定される。
  • 4.M5StickのWebサーバを起動する。

以降、M5StickのWebサーバに「http://wssnano.local/」というドメイン名でアクセスできるようになります。

3.必要な物

3-1. TR4Aシリーズ 又は おんどとりease TR32B

TR4Aは温度や湿度を内部のメモリーに保存し、後からスマホアプリでその値を確認できる商品で、スマホアプリとはBluetooth Low Energyを使用して通信を行います。

今回のプロジェクトの対応機種は下記のとおりです。

  • ・TR41A
  • ・TR42A
  • ・TR43A
  • ・TR41
  • ・TR42
  • ・TR45
  • ・TR32B

TR4Aシリーズ
https://www.tandd.co.jp/product/ondotori/tr4a-series/

おんどとりease TR32B
https://www.tandd.co.jp/product/ondotori/ease/tr32b/

3-2. M5StickC Plus

M5StickC Plusは親指程度のコンパクトなケースの中に、CPU、Bluetooth機能、カラーLCD、無線LAN機能などが搭載されているマイコンボードです。
2023年8月時点で3828円で購入が可能です。

詳細は下記サイトを参照ください。
https://docs.m5stack.com/ja/core/m5stickc_plus

3-3. Arduino IDE

M5StickのプログラムはArduino IDEを使って作成します。
(Arduino IDEではプログラムの事をスケッチと呼ぶため、以後この記事の中ではプログラムの事をスケッチと記述します。)
Arduino IDEはスケッチを書くエディタ、スケッチをM5Stickに書き込むツール、シリアルモニタなどが含まれる総合開発環境(Integrated Development Environment)です。

Arduino IDEのインストールは下記をご参照ください。
https://docs.m5stack.com/ja/quick_start/m5stickc_plus/arduino

インストール直後のArduino IDEのメニューは英語表示となっていますが、下記の設定で日本語表示に変更できます。
メニューから[File]→[Preferences]を選択し、表示された設定画面の Language: の選択肢から[日本語]を選択して[OK]ボタンを押す。
以降の説明は全て 日本語表示として説明します。

その他「M5StickC plus Arduino」などで検索すると参考になる情報が得られると思います。

本記事を執筆時のArduino IDE の動作環境は以下の通りです。

  • ・Arduino IDE バージョン:2.1.0
  • ・ボードマネージャ M5Stack バージョン:2.0.7
  • ・Arduino IDEのメニューで右記を選択:[ツール]→[ボード]→[M5Stick-C-Plus]
  • ・OS:Windows 11 Home

その他、PCとM5Stickを接続するUSBケーブルが必要です。M5StickはUSB-Cコネクタなので必要に応じて用意してください。

3-4. 無線LANアクセスポイント

今回作成するスケッチでは、M5Stickは無線LAN経由でローカルLANに接続してWebサーバとして機能するため、無線LANのアクセスポイントが必要です。
IEEE 802.11b/g/n(2.4GHz)に対応した無線LANのアクセスポイントをご用意ください。
また、スケッチ内にSSIDとパスワードを記載するので、予め控えておいてください。

4.スケッチの説明

4-1. 概要

この章からいよいよM5Stickのスケッチを作成して動作させていきます。
最初に全体の手順を説明します。

  • 1.Arduino IDEで新規にスケッチを作成し、[4-2.スケッチ]で説明する[リスト1]をコピーする。
  • 2.コピーしたスケッチ内の下記部分を各自の環境に書き換える。(4-3. 初期設定値 参照)
    • ・APのSSIDとパスワード
    • ・ブラウザに表示させたい機器のシリアルNo.と名称を設定
  • 3.上記スケッチをコンパイルしてM5Stickに書き込む。
  • 4.M5Stickが起動しLCDに機器のリストが表示されたら、M5Stickと同じAPに繋がっているPCやスマホのブラウザから「http://wssnano.local」にアクセスする。
  • 5.ブラウザの画面に「機器型番」「名称またはシリアルNo.」「温度」「湿度」が表示される。

スケッチを書き始める時はArduino IDEのメニューから[ファイル]→[新規スケッチ] を選択します。すると中身の無い setup() と loop() が作成されるので、その中にスケッチを書いていきます。
setup() は、M5Stickの電源が入ってから最初に1回だけ実行され、loop() は、setup() 実行後に繰り返し実行されます。

今回のスケッチでは、setup() と loop() の他にも関数があります。
全てを入力するのはちょっと大変なので、後ほど記載する[リスト1]をコピペするなどしてスケッチを書いてみましょう。

スケッチの中で「//」が記載されている場合、その行の「//」以降はコメントとなります。

4-2.スケッチ

以下の[リスト1]が今回作成したスケッチの全体です。

スケッチのダウンロード

リスト1

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
#include <M5StickCPlus.h>
#include <BLEDevice.h>
#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>

#define CompanyID 0x0392   // T&D CompanyID

//===== 無線LANアクセスポイント設定(各自の環境を入力) =====
const char* ssid = "xxxxxxxxxxxx";                //(※変更)APのSSID
const char* password = "yyyyyyyyyyyy";     //(※変更)APのパスワード
const char* url_local = "wssnano";          // [url_local].local がブラウザでのアクセス先

typedef struct {
  uint32_t seri;
  char* name;
} Dict;
//===== 名称を表示させたいシリアルNo.と名称を設定(各自の環境を入力) =====
const Dict myDict[8] {
  {0x5F300123,"冷蔵倉庫_南側入口"},                  // 入力例{シリアルNo,"表示させたい名称(MAX全角10文字)"}
  {0x5F314567,"冷凍倉庫_北側入口"},                  // 入力例{シリアルNo,"表示させたい名称(MAX全角10文字)"}
  {0x5F3289AB,"研究室の冷凍冷蔵庫"},               // 入力例{シリアルNo,"表示させたい名称(MAX全角10文字)"}
  {0x00000000,""},                            // 名称を指定しない場合はシリアルNo.0x00000000 と設定する
  {0x00000000,""},                            // 名称を指定しない場合はシリアルNo.0x00000000 と設定する
  {0x00000000,""},                            // 名称を指定しない場合はシリアルNo.0x00000000 と設定する
  {0x00000000,""},                            // 名称を指定しない場合はシリアルNo.0x00000000 と設定する
  {0x00000000,""},                            // 名称を指定しない場合はシリアルNo.0x00000000 と設定する
   };
char buf[16];

struct Device{
  float temp;
  float humi;
  uint32_t seri;
  String model;
  uint8_t tim;
};
struct Device dev[8];

int scanTime = 1;   // In seconds
BLEScan* pBLEScan;
uint8_t adv_cnt;    // アドバタイジングスキャン中に検索した機器数カウント
ulong tm = 0;       // アドバタイジングスキャン間隔タイマ

WebServer server(80);

// Webサーバアクセス時の正常応答をする
void handleRoot(){
  char buf[4000];
  char t_buf[500];
  uint8_t cnt;

  sprintf(buf,
    "<html>\
    <head>\
      <meta http-equiv=\"Refresh\" content=\"60\">\
      <meta charset=\"UTF-8\">\
      <title>WSSnano DataViewer</title>\
      <style media=\"screen\">\
        body{\
          background: #eef4fa;\
          padding:28px 32px;\
          margin:0;\
        }\
        #wrapper{\
          border-radius: 16px;\
          background: #fff;\
          box-shadow: 0px 10px 32px rgba(188, 208, 224, 0.4);\
          padding:30px;\
          margin:0;\
          overflow-x:scroll;\
        }\
        #wrapper h1{\
          font-size: 32px;\
          line-height:1.2;\
          color: #254569;\
          font-weight:700;\
          margin:0;\
          padding:0;\
        }\
        #wrapper table{\
          width:100%%;\
          margin:25px 0 0 0; \
          padding:0;\
          border-collapse: collapse;\
          border-spacing: 0;\
        }\
        #wrapper table th{\
          font-size: 16px;\
          text-align: center;\
          color: #fff;\
          background: #3180c4;\
          padding:5px 23px 0 ;\
          height:40px;\
          box-sizing:border-box;\
          width:20%%;\
          vertical-align:middle;\
          line-height:1.2;\
        }\
        #wrapper table th.th1{\
          width:40%%; \
          text-align:left;\
        }\
        #wrapper table td{\
          font-size: 22px;\
          text-align: center;\
          color: #586b7f;\
          background: #FFF;\
          padding:3px 23px 0 ;\
          height:56px;\
          box-sizing:border-box;\
          vertical-align:middle;\
          line-height:1.2;\
        }\
        #wrapper table td.td1{\
          text-align:left; \
        }\
        #wrapper table tbody tr:nth-child(2n) td{\
          background: #f9fbfd;\
        }\
      </style>\
    </head>\
    <body>\
      <div id='wrapper'>\
        <h1>WSSnano DataViewer</h1>\
      <table>\
        <thead>\
          <tr>\
            <th class='th1'>MODEL</th>\
            <th>NAME</th>\
            <th>Temperature[C]</th>\          
            <th>Humidity[%%]</th>\
          </tr>\
        </thead>\
        <tbody>");

    for(cnt=0 ; cnt < 8; cnt++){
      if(dev[cnt].seri != 0){
        sprintf(t_buf,
          "<tr >\
          <td class='td1'>%s</td>\
          <td>%s</td>\
          ",dev[cnt].model, Ditc_srh(dev[cnt].seri));
          strcat(buf,t_buf);

        if(dev[cnt].temp == 20001){
          sprintf(t_buf,
            "<td>----</td>\
            ");
        }
        else if(dev[cnt].temp == 20000){
          sprintf(t_buf,
            "<td> </td>\
            ");
        }
        else {
          sprintf(t_buf,
            "<td>%3.1f</td>\
            ",dev[cnt].temp);
        }
        strcat(buf,t_buf);

        if(dev[cnt].humi == 20000){
          sprintf(t_buf,
            "<td> </td>\
            </tr>\
            ");
        }
        else if(dev[cnt].humi == 20001){
          sprintf(t_buf,
            "<td>----</td>\
            </tr>\
            ");
        }
        else {
          sprintf(t_buf,
            "<td>%3.0f</td>\
            </tr>\
            ",dev[cnt].humi);
        }
        strcat(buf,t_buf);
      }
    }

    strcat(buf,
      "</tbody>\
      </table>\
    </body>\
    </html>");

  server.send(200,"text/html",buf);
  M5.Lcd.setTextColor(WHITE,BLUE);
  M5.Lcd.drawString(" accessed on \"/\"  ",133,0,2);
}

// Webサーバアクセス時のエラー応答をする
void handleNotFound(){
  server.send(404,"text/plain","File Not Found");
  M5.Lcd.setTextColor(WHITE,RED);
  M5.Lcd.drawString(" File Not Found   ",133,0,2);
}

// シリアルNo → HTTP表示 する名称を決定する
char* Ditc_srh(uint32_t seri){
  uint8_t cnt;
  sprintf(buf,"SN:%08X\0",seri);    // シリアルNo.を文字列に変換する
  Serial.printf("buf:%s\n", buf);
  for(cnt = 0 ; cnt < 8 ; cnt++){
    if(seri == myDict[cnt].seri){   // シリアルNo→名称 の変換データが存在する場合、
      return(myDict[cnt].name);     //   名称をHTML表示
    }
  }
  return(buf);                      // シリアルNo→名称 の変換データがない場合シリアルNoをHTML表示
}

// 受信データの保存領域(8台分)に空きがあるか検索
uint8_t dev_data_srh(uint32_t seri){
  uint8_t cnt, emp_no = 0xff, rtn = 0xff;
  for(cnt = 0 ; cnt < 8 ; cnt++){               // 受信データの保存領域8個分繰り返し
    if(dev[cnt].seri == seri){                  // 受信データの保存領域に既にデータがある場合は
      rtn = cnt;                                // データを上書きする為にそのアドレスを返す。
      break;
    }
    if((dev[cnt].seri == 0)&&(cnt < emp_no)){   // 空領域の最小アドレを探す
      emp_no = cnt;                             // 空領域のアドレスを保存する
    }
  }
  if(rtn == 0xff)rtn = emp_no;                  // 既存データがない場合は空領域アドレスを返す
  return rtn;
}

// シリアルNo.から機種名と機器ごとの情報を取得する
String Find_model(uint32_t seri,uint8_t *flg){
// *flg に機器の下記情報を返す
// bit0: 温度測定 0=無、1=有
// bit1: 湿度測定 0=無、1=有
// bit2: アドバタイズパケットの測定値の位置 0=10、1=8
// bit3-7:未使用
  String rtn;
  uint8_t seri2 = (uint8_t)(seri >> 16);
  rtn = "xxxxx";
  if((seri2 == 0x40)||(seri2 == 0x41)){rtn = "TR41A"; *flg = 0x01;}       // シリアルNo→機種名
  else if((seri2 == 0x42)||(seri2 == 0x43)){rtn = "TR42A"; *flg = 0x01;}
  else if((seri2 == 0x44)||(seri2 == 0x45)){rtn = "TR43A"; *flg = 0x03;}
  else if((seri2 == 0x46)||(seri2 == 0x47)){rtn = "TR32B"; *flg = 0x03;}
  else if((seri2 == 0x2C)||(seri2 == 0x2D)){rtn = "TR41 "; *flg = 0x05;}
  else if((seri2 == 0x2E)||(seri2 == 0x2F)){rtn = "TR42 "; *flg = 0x05;}
  else if((seri2 == 0x30)||(seri2 == 0x31)){rtn = "TR45 "; *flg = 0x05;}
  return(rtn);                      // シリアルNo→model名
}

// アドバタイジングスキャン中の1機器ごとの割り込み
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
      if(adv_cnt++ > 100){
        pBLEScan->stop();
      }
      //Serial.printf("Advertised Device: %s \n", advertisedDevice.toString().c_str());
    }
};

void setup() {
  M5.begin();
  Serial.begin(115200);           // シリアル通信初期設定

  M5.Lcd.setRotation(3);          // LCD表示初期設定
  M5.Lcd.setCursor(0, 0, 4);      // 表示位置とフォントを指定
  M5.Lcd.print("Connecting..");   // 最初にConnecting..と表示する

  WiFi.begin(ssid, password);     // 無線LANアクセスポイント接続開始
  while (WiFi.status() != WL_CONNECTED) { // 接続完了を確認
    M5.Lcd.print(".");                    // 接続途中の表示
    delay(300);                           // 300msec間隔で接続完了を確認
  }
  M5.Lcd.println("\nConnected to");     // DHCPで得られたIPアドレスを表示
  M5.Lcd.println(WiFi.localIP());       // DHCPで得られたIPアドレスを表示

  if(MDNS.begin(url_local)){            // Webサーバアドレス [nanowss.local]
  //  M5.Lcd.println("MDNS started");   // DHCPで得られたIPアドレスを表示
  }
  server.on("/",handleRoot);
  server.onNotFound(handleNotFound);  
  server.begin();
  M5.Lcd.println("WebServer started");  // DHCPで得られたIPアドレスを表示

  BLEDevice::init("");              // Bluetooth Low Energy初期設定
  pBLEScan = BLEDevice::getScan();  // BLEスキャンオブジェクト取得
  pBLEScan->setActiveScan(false);   // パッシブスキャン
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setInterval(100);
  pBLEScan->setWindow(99);
  M5.Lcd.println("Scanning...");    // アドバタイジングスキャン中の表示

  uint8_t cnt, flg;
  for(cnt = 0 ; cnt < 8 ; cnt++){
    if(myDict[cnt].seri != 0x00000000){
      dev[cnt].model = Find_model(myDict[cnt].seri, &flg);
      dev[cnt].temp = 20001;                        // 温度「----」
      if((flg & 0x02) == 0)dev[cnt].humi = 20000;   // 湿度「    」
      else dev[cnt].humi = 20001;                   // 湿度「----」
      dev[cnt].seri = myDict[cnt].seri;
      dev[cnt].tim = 7;
    }
  }
}

void loop() {
  char seri_tmp[10];
  uint8_t cnt, i, count, flg, dt_adr;
  uint32_t now_seri;

  if(tm == 0)tm = millis() + 30000;     // 更新する時間を30秒後に設定する

  for(cnt = 0 ; cnt < 10 ; cnt++){      // アドバタイジングスキャンは30秒ごとに[1秒x10回]行う
    adv_cnt = 0;                        // アドバタイジングスキャンで検索した機器台数のカウンタ
    BLEScanResults foundDevices = pBLEScan->start(scanTime,false);   // アドバタイジングスキャン開始
    count = foundDevices.getCount();    // count=スキャンで見つけたデバイス数
    for(i = 0 ; i < count ; i++){       // デバイスの個数の回数繰り返す
      BLEAdvertisedDevice d = foundDevices.getDevice(i);
      if(d.haveManufacturerData()){                  
        //### アドバタイジングパケットにManufacturerDataが含まれる場合以下実行 ###
        std::string data = d.getManufacturerData(); // data=受信したManufacturerData
        int manu = data[1] << 8 | data[0];          // manu=受信したCompany ID
        if(manu == CompanyID){  
          //### ManufacturerDataがT&D製品ならば以下実行 ###
          now_seri = ((uint32_t)data[5]<<24)+
            ((uint32_t)data[4]<<16)+((uint32_t)data[3]<<8)+data[2]; // now_siri=受信機器のシリアルNo.
          String mdl_tp = Find_model(now_seri, &flg);
          if(mdl_tp != "xxxxx"){
            uint8_t d_id = dev_data_srh(now_seri);      // 受信データの保存領域の空きを探す
            if(d_id != 0xff){   // 表示用のメモリに空きがあれば保存する
              if((flg & 0x04) == 0)dt_adr = 10;     // アドバタイジングパケットの測定値位置を機種により指定
              else dt_adr = 8;                      // アドバタイジングパケットの測定値位置を機種により指定
              dev[d_id].seri = now_seri;                // ManufacturerDataのシリアルNo.保存
              dev[d_id].temp = data[dt_adr+1] << 8 | data[dt_adr];    //- ManufacturerData →温度データ取得
              if(dev[d_id].temp > 20000)dev[d_id].temp = 20001;
              else dev[d_id].temp = (dev[d_id].temp - 1000) / 10;         // 温度データをXX.X[℃]の値に変換
              dev[d_id].humi = data[dt_adr+3] << 8 | data[dt_adr+2];  //- ManufacturerData →湿度データ取得

              if((flg & 0x02) == 0)dev[d_id].humi = 20000;  // 湿度がない機器の湿度値は20000とする
              else{
                if(dev[d_id].humi > 20000)dev[d_id].humi = 20001;
                else dev[d_id].humi = (dev[d_id].humi - 1000) / 10;       // 湿度データをXX.X[%]の値に変換
              }
              dev[d_id].model = mdl_tp;
              dev[d_id].tim = 7;      // 未受信になった場合の表示を継続する回数を指定
            }
          }
        }
      }
    }
    server.handleClient();
  }

  M5.Lcd.setTextColor(BLACK,CYAN);
  M5.Lcd.drawString("  Receivable device                   ",0,0,2);  // 受信機器リストのタイトル表示
  M5.Lcd.fillRect(0,15,240,160,BLACK);
  uint8_t y_line = 0;
  for(i = 0 ; i < 8 ; i++){            // デバイスの個数の回数繰り返す
    Serial.printf("%s S/N:%8X  ",dev[i].model, dev[i].seri);  //(8)
    Serial.printf("temp:%3.1f humi:%3.0f tim:%d\n",dev[i].temp,dev[i].humi,dev[i].tim); //
    M5.Lcd.setTextColor(WHITE,BLACK);
    if(dev[i].seri != 0){       // 受信データの保存領域にデータが入っている場合表示する
      y_line++;
      M5.Lcd.setCursor(10,15*(y_line),2);               // シリアルNo表示位置とフォントを指定
      M5.Lcd.printf("%s %8X",dev[i].model, dev[i].seri); // シリアルNo表示
      M5.Lcd.setCursor(145,15*(y_line),2);              // 温度の表示位置とフォントを指定
      if(dev[i].temp == 20001)M5.Lcd.printf(" ----");              // 温度を表示
      else M5.Lcd.printf("%3.1fC",dev[i].temp);              // 温度を表示

      if(dev[i].humi != 20000){
        M5.Lcd.setCursor(190,15*(y_line),2);            // 湿度の表示位置とフォントを指定
        if(dev[i].humi == 20001)M5.Lcd.printf(" ----");
        else M5.Lcd.printf("%3.0f%%",dev[i].humi);           // 湿度を表示
      }
    }
    if(dev[i].tim > 0){   // 180sec(30sec x 6)未受信だったら表示リストから消去
      if(--dev[i].tim == 0){
        dev[i].seri = 0; dev[i].temp = 0; dev[i].humi = 0;  // 受信データの保存領域をクリア
        dev[i].model = "";                                   // 受信データの保存領域をクリア
      }
    }
  }
  while(millis() < tm){   // アドバタイジングスキャンの間隔待つ
    server.handleClient();
    delay(100);
  }
  tm = 0;
}
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
            

[リスト1]のスケッチは、[表1]に示した関数などで構成されています。

表1

行番号 関数名 機能
10~28 初期設定 ・無線LANアクセスポイントの設定。
・WebサーバのURLの設定。
・シリアルNo.を別名称で表示する設定。
48 void handleRoot() ブラウザからM5StickにHTTPのリクエストがあったときにレスポンスを応答する関数。
レスポンス可能なTR4Aの台数は最大8台までとする。
197 void handleNotFound() ブラウザからM5Stickに存在しないファイルのリクエストがあった場合にエラーレスポンスを応答する関数。
204 char* Ditc_srh() TR4AのシリアルNo.を予め設定した名称に変換する関数。
ブラウザに表示される名称はこの関数で変換された名称。
217 uint8_t dev_data_srh() TR4Aから取得した測定値を保存する領域を決める関数。
保存領域は8台分あり、シリアルNo.毎に異なる領域に測定値を保存する。
233 String Find_model() 機器のシリアルNo.から機種名を調べる関数。
機種名と共に、機種による異なる測定項目(湿度測定の有無)や、アドバタイジングパケットに含まれる測定値データの位置を返す。
254 void onResult() アドバタイジングのスキャンをしている最中に1個検索されるたびに実行される関数。
実行されるごとにadv_cntというカウンタを加算し、100を超えた場合はアドバタイジングのスキャンを停止する。
262 void setup() スケッチを起動したときに最初に1回だけ実行される関数。
M5StickのカラーLCD表示、Bluetooth通信、Webサーバ等の初期設定を行う。
307 void loop() M5Stickの起動中は連続して常に実行される関数。
1分ごとにアドバタイジングスキャンを行い、受信した測定値を保存する。

Webサーバへのリクエストもこの中で待ち受け、リクエストを受信すると「handleRoot()」を実行してレスポンス応答する。

次に各関数について説明していきます。

4-3. 初期設定値

スケッチの下記の部分は使用される環境により変更が必要です。(表2)
特にssidとpasswordは必ず変更が必要です。

表2

行番号 設定項目 機能
10 ssid= 無線LANアクセスポイントのSSID
(3-3. 無線LANアクセスポイント 参照)
11 password= 無線LANアクセスポイントのパスワード
(3-3. 無線LANアクセスポイント 参照)
12 url_local= ブラウザからアクセスするURL
[ここで指定した文字列].local がアドレスになります。
20~27 ブラウザで表示する名称を設定 ブラウザに表示する機器ごとの名称を任意に設定します。
機器のシリアルNo.と表示させたい名称を下記のように指定します。(名称は全角10文字以内)
 { }内の値を、下記のように指定します。
 {[シリアルNo.を16進数で記載],"[ブラウザに表示させたい名称]"},

<<例>>
シリアルNo.:5F4601AB の機器を「リビングルームの南側」と表示させたい。
下記のように記載
{0x5F4601AB, "リビングルームの南側"},

4-4. handleRoot について

[リスト1]48行の handleRoot() は、ブラウザからM5Stickへアクセスされたときに応答を返します。
ブラウザから[wssnano.local/]にアクセスするとTR4Aから取得した測定値を表として表示します。
(wssnano.local/ の「wssnano」は好みのアドレスに変更可能です。4-3.初期設定値 url_localを参照)

図2:「handleRoot」フローチャート

[リスト1]56行の「<meta http-equiv=\"Refresh\" content=\"60\">」は、HTMLのタグで、現在のアドレスを60秒ごとに再読み込みする命令です。
この記述により、ブラウザ画面の測定値が自動的に最新の表示に更新されます。

4-5. handleNotFound について

[リスト1]197行の handleNotFound() は、ブラウザからM5StickのWebサーバの[wssnano.local/]以外にアクセスすると実行され、エラー応答を返します。

4-6. Dict_srh について

[リスト1]204行の Dict_srh() は、機器のシリアルNo.を元に、ブラウザで表示する機器の名称を返す関数です。
4-3.初期設定値の「ブラウザで表示する名称を設定」によりシリアルNo.を名称に変換します。
名称を指定していない機器は、シリアルNo.をそのまま文字列にして返します。

4-7. dev_data_srh について

今回のスケッチでは、アドバタイジングパケットを受信した場合、それをブラウザで表示させるために保存するメモリを8台分用意しています。(dev[0]~[8])
このメモリを「保存領域」とよび、8台分をそれぞれ「アドレス=0〜7」の8台分と記述します。

[リスト1]217行の dev_data_srh() は、シリアルNo.から保存領域のアドレスを決定します。
既に受信したことがあるシリアルNo.なら以前と同じアドレスを返し、初めて受信した場合は未使用のアドレスを返します。

4-8. Find_model について

[リスト1]233行の Find_model() は、機器のシリアルNo.から、機種ごとに異なる機種名称、アドバタイジングに含まれる測定値の内容や参照するデータの位置などを把握することができます。

4-9. onResult について

[リスト1]254行の onResult() は、Bluetooth Low Energyのアドバタイジングスキャンを行っている最中にデータを受信するたびに実行される関数です。
周辺にアドバタイジングパケットを送信している機器が多数あったときにM5Stickが再起動してしまうことがあったので今回のスケッチではこの関数を用いて100台以上受信した場合はアドバタイジングスキャンを終了する処理をしています。

4-10. setup について

setup() では、M5StickでBluetoothやカラーLCDなどを動作させるための初期設定を行っています。
フローチャートは[図3]に示します。

[リスト1]264行の Serial.bigin(115200) は115200bpsでシリアル通信するための初期設定です。シリアル通信はスケッチの動作確認のために使用し、Arduino IDEのメニューから [ツール]→[シリアルモニタ]で開いた画面に動作状況を出力させることができます。
Serial.println("Connecting..") と記述するとシリアルモニタに「Connecting..」と表示されます。
スケッチの途中に上記のような記述を追加しておく事で、スケッチが実行されている途中状況を確認することができる仕組みです。

[リスト1]268行の M5.Lcd.xxxxxxx という部分はM5StickのLCD表示関連のコマンドで、LCDの初期設定と「Connecting..」と表示させる処理を行っています。

[リスト1]270行の WiFi.begin(ssid, password)では、M5Stickが無線LANのアクセスポイントへの接続を開始します。接続が成功した場合、276行でDHCPで得られたIPアドレスを表示しています。

[リスト1]278行の MDNS.begin(url_local) では、マルチキャストDNSという仕組みを使って、DHCPで得られたIPアドレスを「nanowss.local」(任意のアドレスに変更も可能)というアドレスで呼べるように指定しています。

[リスト1]286〜291行はBluetoothの設定を行っています。
ここではパッシブスキャンでアドバタイジングパケットを受信する設定を行っています。

図:3 「setup」フローチャート

4-11. loop について

loop()スケッチ動作中に繰り返して実行される部分です。
フローチャートは[図4]に示します。

[リスト1]312行は、アドバタイジングスキャンと表示更新を30秒間隔で行うためのタイマを設定しています。

[リスト1]316行の pBLEScan->start(scanTime) ではアドバタイジングパケットのパッシブスキャンを起動しています。「scanTime」はスキャンをする時間を秒で指定します。今回は1秒を指定し、それを10回繰り返し、約10秒間アドバタイジングスキャンを行っています。

1秒間のアドバタイジングスキャンごとに受信したデータからManufacturreDataを探し、その中の「Company ID=0x0392(T&D)」と機器のシリアルNo.からTR4Aであることを判断します。
ManufacturreDataの内容は、[表3]、[表4]を参照してください。

シリアルNo.から機種名を把握するために、[リスト1]328行で Find_model()関数を呼び出しています。
機種が決まった後に、[リスト1]330行のdev_data_srh(now_seri) によりデータ保存領域のアドレスを取得し、そこに測定値などを保存します。

アドバタイジングスキャン終了後、[リスト1]360〜376行で受信した機器のシリアルNo.と測定値などをM5StickのLCDに表示しています。

LCDに表示した後の[リスト1]377〜382行では、機器ごとに表示した回数をカウントダウンしています。アドバタイジングスキャンで受信できる機器は連続して表示を続けますが、受信できない状態が180秒継続すると、表示から消去する処理をしています。

[リスト1]384~387行では、
30秒ごとにアドバタイジングスキャンと表示の更新を行うための待ち時間を設けています。

表3 TR4Aシリーズ/TR32BのManufacturerData

byte位置 形式 内容  
0~1 Uint16 Company ID 0x0392→T&D
2~5 Uint32 機器シリアルNo. TR32B: 0xXX46XXXX or 0xXX47XXXX
TR41A: 0xXX40XXXX or 0xXX41XXXX
TR42A: 0xXX42XXXX or 0xXX43XXXX
TR43A: 0xXX44XXXX or 0xXX45XXXX
6~9 省略 省略 省略
10~11 Uint16 測定値1 温度 測定値
12~13 Uint16 測定値2 湿度 測定値
14~19 省略 省略 省略

表4 TR4シリーズのManufacturerData

byte位置 形式 内容  
0~1 Uint16 Company ID 0x0392→T&D
2~5 Uint32 機器シリアルNo. TR41: 0xXX2CXXXX or 0xXX2DXXXX
TR42: 0xXX2EXXXX or 0xXX2FXXXX
TR45: 0xXX30XXXX or 0xXX31XXXX
6~7 省略 省略 省略
8~9 Uint16 測定値1 温度 測定値
10~19 省略 省略 省略
図4:「loop」フローチャート

5.実行

5-1. コンパイルと書き込み

今回のスケッチはサイズが大きいため、M5Stickのプログラムを書き込むメモリサイズを増やすために下記の設定を行います。
「ツール」→「Partition Scheme:」の項目で「No OTA(Large APP)」

スケッチの入力が終わったら、PCとM5StickをUSBケーブルで接続し、Arduino IDEのメニューから [スケッチ]→[書き込み] を実行します。(又は上部の丸い右矢印(→)のスイッチを押下)

実行するとPCの画面右下に「スケッチをコンパイル中...」→「書き込み中」→「書き込み完了」と処理状態を示すメッセージが表示され、状態が変化していきます。
「書き込み完了」と表示された場合、M5Stickは自動的にスケッチ通りの動作を開始します。

ただし、途中でエラーが発生した場合、「書き込み完了」に至らない場合もあります。
その場合、よくある例として[表5]の状況が考えられるので確認してみてください。

表5 Arduino IDEエラー例

Arduino IDE
画面下部の出力の表示
原因/対処内容
exit status 1
・・・
プログラムの記述が間違っている。
「exit status 1」以下の記述やリストの色が変わっている箇所を確認。

メニューの [ツール]→[ボード] でM5Stick-C-Plus が選択されているか確認。
M5Stick-C-Plusが選択肢にない場合、下記を参考にM5Stackライブラリをインストールする。
https://docs.m5stack.com/en/quick_start/m5stickc_plus/arduino
Failed uploading: no upload port provided M5StickがPCとUSBケーブルで接続されているか確認。
[ツール]→[ポート]: M5Stickが接続されているCOMポートの選択を確認。
スケッチが大きすぎます。 「ツール」→「Partition Scheme:」の項目で「No OTA(Large APP)」を指定する。
Failed uploading: uploading error:
exit status 1
M5Stickへのスケッチの書き込みに失敗しました。
下記の様に書き込み時の通信スピードを下げて確認。

メニューの[ツール]→[Upload Speed]→[115200]

5-2. 動作確認

M5Stickでスケッチが動作し始めると、M5StickのLCDに「Connecting...」と表示されます。

無線LANのアクセスポイントに接続できると、DHCPサーバから取得したIPアドレスを表示し、Webサーバを起動します。
続けてアドバタイジングスキャンを開始し「Scannng...」と表示されます。

アドバタイジングスキャンが終了すると検索された機器のリストを表示します。

同じネットワークに接続されたPCなどのブラウザで [http://wssnano.local/] にアクセスし、機器のリストが表示されれば成功です。

※アクセスに失敗する場合は、M5Stick起動時にLCDに表示されるIPアドレスにアクセスしてみてください。

M5StickをPCに接続した状態で、Arduino IDEのメニューから [ツール]→[シリアルモニタ] を実行し、シリアルモニタ画面右上の設定を「115200 baud」に変更してみてください。
スケッチの中で Serial.print… と記述している内容がシリアルモニタに表示され、スケッチの実行状況を確認することができます。

6.最後に

今回は、M5Stickを使っておんどとりのデータをBluetoothで受信し、ブラウザで可視化させてみました。

M5StickがBluetoothで取得したデータを Bluettoth → LAN と異なる通信手段に中継する動作を行います。
この方法を応用すると、今回のようにブラウザで見るだけにとどまらず、データをサーバで保存したり、加工するなど、更に発展させる可能性も広がります。

記事の内容を参考に自由な発想でTR4AシリーズやTR32Bを活用していただくきっかけとなれば嬉しいです。

免責

本記事の中で紹介したスケッチは、全ての環境、条件において動作する保証をするものではありません。
また、記載されている情報の利用やスケッチの実行等による万一の不利益に対して、弊社では責任を負いかねます。

プライバシー設定

プライバシー設定

当ウェブサイトでは、サイトの利便性やサービスの向上を目的に、cookieを使用しております。このまま当ウェブサイトをご利用になる場合、cookieの使用に同意いただいたものとみなされます。cookieに関する詳細や設定については「個人情報保護方針」をご覧ください。

保存する