PR

ESP32-WROOM-32EとTWELITEの特徴を活かした無線通信

組み込みエンジニア
本記事はプロモーションが含まれています。

こんにちは、ENGかぴです。

ESP32-WROOM-32EとTWELITE(トワイライト)でWiFi通信とZigBee通信の特徴を活かした通信システムを作ることができます。ZigBee通信で子機と親機の通信を行い、WiFi通信で温湿度センサーの情報を表示する方法をまとめました。

温湿度センサーであるSHT35とトワイライト(以下TWELITEとする)を組み合わせた子機で温湿度情報をZigBee通信し、ESP32-WROOM-32E(以下ESP32とする)とTWELITEを組み合わせた親機で温湿度情報を無線受信します。ESP32はWebサーバーを実装して温湿度情報をグラフ表示します。

ESP32-WROOM-32E開発ボード(秋月電子)を使用しArduino IDEで開発を行います。温湿度センサーはAE-SHT35(秋月電子:以下SHT35とする)を使用しています。 TWELITEはTWELITE-DIP-RED(モノワイヤレス)を使用しています。

ESP32を使って動作確認したことをまとめています。

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

TWELITEで動作確認したことについてまとめています。

トワイライト(TWELITE)のソフト開発と無線通信でできること

ESP32とTWELITEの関係

ESP32とTWELITEの関係
ESP32とTWELITEの関係

ESP32とTWELITEの関係を説明します。ESP32とTWELITEは無線通信モジュールです。ESP32はWiFi通信が使用できるため大容量のデータを伝送することができますが、電波が届きにくく近距離通信になってしまいます。

TWELITEはZigBee通信で一度に通信できるデータ量は少なくなりますが、低消費電流で中距離通信できます。電波強度が高いTWELITE-REDを使用すると見通し距離で1km~2kmの通信ができます。

WiFi通信の大容量のデータが伝送できる特徴とZigBee通信の低消費電流で中距離通信ができる特徴を組み合わせることで、WiFi通信では届かない範囲のセンサーのデータを取得することができます。

子機のTWELITEはSHT35からI2C通信で温湿度データを取得し、一定周期で親機のTWELITEに無線送信します。

親機のTWELITEは子機から受信したデータをESP32にシリアル通信で送信します。ESP32は取得した温湿度データを履歴保存しながらスマホなどの通信端末(クライアント)からの接続を待機します。クライアントから接続要求を受けるとWebサーバーが応答して温湿度データの履歴を表示します。

参考記事としてTWELITEとArduino UNOをシリアル通信する方法をまとめています。ESP32でも同様の方法でシリアル通信できます。

Arduinoとトワイライト(TWELITE)でセンサー情報を表示する

広告
漠然としたキャリア形成の不安を打ち破る!

親機の構成

親機の構成
親機の構成

ESP32とTWELITEを組み合わせた親機の構成です。回路図の番号はESP32の左上を1ピンとした時反時計回りにピンを数えた場合の番号としています。ピン番号横の()内の番号はシルク印刷されているピンの名称です。

TWELITEの電源はESP32の3V3(3.3V)とGNDを使用します。ESP32はTWELITEからシリアル通信で電文を受信するためRX2を使用します。TWELITEはESP32に電文を送信するためTXを使用します。

ESP32はWebサーバーでクライアントからの接続を待機します。接続要求があればHTTPデータで応答し温湿度データの履歴を表示します。

ESP32の参考記事

温湿度データの取得方法及びWebサーバーを実装する方法を下記記事にまとめています。

ESP32-WROOM-32EでWebServerを実装する

温湿度データの履歴を表示する前提としてChartライブラリをESP32のFLASHに書き込む必要があります。Chartライブラリを書き込む方法を下記記事にまとめています。

ESP32-WROOM-32EにSDカードのファイルをアップロードする

Chart.jsを使ってブラウザー上にグラフ表示する方法を下記記事まとめています。

Chart.jsを使ってデータをブラウザー上でグラフ表示する

以下の説明はChartライブラリがFLASHに書き込まれていることが前提です。

SPIFFSでFLASHのファイルを参照する

FLASHのファイルにアクセスするためにSPIFFSライブラリを使用します。SPIFFSライブラリの初期化とWebサーバーがファイルを参照できるようにします。

#include <SPIFFS.h>
WebServer Wserver(80);

SPIFFS.begin();
Wserver.serveStatic("/myfavicon.ico",SPIFFS,"/myfavicon.ico");
Wserver.serveStatic("/chart.min.js",SPIFFS, "/chart.umd.js");

SPIFFSライブラリを使用するためにSPIFFS.hをインクルードします。begin()関数でSPIFFSに関する初期化を行います。

WebサーバーライブラリのserverStatic()関数でFLASHに書き込んだChartライブラリやファビコンのファイルをSPIFFSを使って参照できるようにします。

スポンサーリンク

HTMLデータの生成

void HtmlSet(void){
  String str = "";

  str += "<head>";
  str += "<link rel='shortcut icon' href='/myfavicon.ico' />"; //ファビコンを指定
  str += "<script src = '/chart.umd.js'></script>"; //chart.umd.jsを読み込み 
  str += "</head>";

HTMLデータを生成してブラウザに表示します。HTMLデータの<head>部分にファビコンを使用する場合は使用するファイルを指定します。<script>でchart.umd.jsを参照する指定を行います。前述でWebサーバーがFLASHのファイルを参照できるChartライブラリが使用できるようになります。

float tempdat[CHART_SZ];//温度の履歴の配列
String str="";

str += "datasets: [{";
str += "label: '温度',";
str += "fill: false,";
str += "borderColor: 'red',";
str += "borderWidth: 1,";
str += "pointRadius: 0,";
str += "pointHoverBorderWidth: 10,";
str += "data: [";
  for(uint16_t i = 0; i< CHART_SZ; i++ ){
    str += tempdat[i]; 
    if(i != CHART_SZ-1 )str += ",";
  }
str += "]";
str += "}, {";

Chart.jsはHTTPファイルと関連付けられた配列であればX軸やY軸の値を配列名で指定するとグラフ表示ができますが、温湿度の履歴はESP32のRAMに格納しているため関連性がなく配列名を直接指定することができません。

11~16行目のように配列を文字列で追加してグラフのデータをセットする必要があります。例のようにdata[]に配列のデータを追加してHTMLデータを生成しています。

例)文字列追加の様子
配列の一番目を追加:data[25.03,
配列の二番目を追加:data[25.03,25.05,
配列のN番目を追加:data[25.03,25.05,・・・,25.07,
配列の追加が完了:data[25.03,25.05,・・・,25.07,・・・,25.04]

一文字でも間違うと表示できなくなるためテキストファイルなどでHTML形式で文章を生成しGoogle Chromeで開いて表示できることを確認しておくとよいでしょう。

ESP32のString型は65535バイト分(2バイト)までしか対応していないため文字の増やし過ぎには注意が必要です。 RAM領域を圧迫してスタックオーバーする可能性もあるため文字列が大きくならないように管理が必要です。

PR:【CreatorsFactory】転職率96%!Webスクール説明会申し込み

シリアル通信に使用する電文

シリアル通信の電文の構成
シリアル通信の電文の構成

ESP32とTWELITE間のシリアル通信に使用する電文です。ヘッダは先頭を示すために使用します。アスキー文字で’T’、’W’の2バイト構成にしていますが、本記事の電文の判定には’T’のみを使用しています。

サイズはCHから湿度までのデータ部のバイト数とします。CHは子機が複数あるときに子機の番号を識別するために使用します。

タイムスタンプはTWELITEの親機が子機から受信した時点でのカウント値を4バイトでセットします。

温度、湿度はそれぞれ2バイトデータです。TWELITEの子機から100倍値のデータで無線受信するためこれらの値を100で割ると温度、湿度のデータに換算できます。

SUMはヘッダから湿度までのすべてのデータを加算していた結果の下位1バイトのデータをセットします。

TWELITEの処理

TWELITEの親機は子機から無線受信するとESP32にシリアル通信でデータを送信します。下記記事にアプリケーションIDやチャンネル番号の設定など親機の処理についてまとめています。

トワイライト(TWELITE)で親機を実装し子機から無線受信する

本記事ではアプリケーションID及びチャンネル番号の一致を確認して温度、湿度の情報を無線受信しています。

//TWELITE親機の無線受信処理
void receive() {
  uint16_t temp;
  uint16_t humi;

  auto&& rx = the_twelite.receiver.read(); //データを受信
  auto&& np = expand_bytes(rx.get_payload().begin(),rx.get_payload().end()
          ,temp
          ,humi
  );

  txdata.cnl = int(rx.get_addr_src_lid()); //子機のアドレス
  txdata.temp[0] = temp & 0xFF;
  txdata.temp[1] = temp >> 8;
  txdata.humi[0] = humi & 0xFF;
  txdata.humi[1] = humi >> 8;
  SerialTxSet(); //データを送信する
}

TWELITEの受信機が無線受信したパケットからデータを取得します。

the_twelite.receiver.read()関数でパケットのデータを読み込みます。expand_bytes()関数でパケットのペイロード(ユーザーが指定したデータ)を取得し、temp,humiに格納します。

txdataは送信用のデータを格納する変数です。txdataに送信用のデータをセットします。例ではrx.get_addr_src_lid()関数で取得した子機のアドレスと温度湿度のデータを指定しています。温度(temp)、湿度(humi)は2バイトデータで構成しているので1バイトデータに置き換えてセットします。

void SerialTxSet(void){
  uint8_t *adrs;

  adrs =&txdata.header[0];
  for(uint8_t i = 0; i < allsz; i++ ){//電文のサイズ
    Serial << (*adrs);
    adrs++;
  }
}

ポインタでtxdataの先頭(ヘッダ)のアドレスを指定し、ポインタを更新しながらSerialでデータ(*adrsはポインタに格納している値)を送信します。

PR:技術系の通信教育講座ならJTEX

ESP32の処理(シリアル通信)

ESP32はTWELITEから受信した電文を処理します。下記記事にTWELITEとArduino UNO間でシリアル通信する方法をまとめています。

トワイライト(TWELITE)とArduino間でシリアル通信する

参考記事と同様の考え方ですが、電文の構成を変更してESP32とTWELITEの間でシリアル通信を行います。ESP32はTWELITEから受信した電文に合致する場合に電文を受け入れます。

void Rcvmain(void){
 
  if( esp32Rcv.buf[ rp ] != HEADER_1 ){ //ヘッダの確認
    //電文のヘッダーが不一致
  }
  else{
    //受信したデータのサイズ確認
    sumchk = CalcSum(&Rcvdata[3], datsz); //SUMの計算
    if( sumchk == Rcvdata[allsz-1]){//SUMのチェック
      Sht35Set(true);  //履歴データにセット
    }
}

受信データのスタートがHEADER1(’T’)であれば電文のスタートとみなします。ヘッダの後はサイズが続くためサイズを確認して電文の全体のサイズを確認します。

受信したデータをサイズ分確認しますが、取得したデータの健全性を確認するため取得したデータのSUMを計算し、電文の最後に付加しているSUMと一致するか確認します。SUMが一致すると正常なデータとして温湿度データを履歴データに格納します。

履歴データを準備する

uint16_t temp=0;
uint16_t humi=0;
float temp_f;
float humi_f;

  temp = (Rcvdata[9] << 8) + Rcvdata[8]; //2byteデータを生成
  humi = (Rcvdata[11] << 8) + Rcvdata[10];
  temp_f = (float)temp/100; //100倍値なので100で割って換算
  humi_f = (float)humi/100;

  memmove( &tempdat[1], &tempdat[0],(CHART_SZ-1)*sizeof(float));
  memmove( &humiddat[1], &humiddat[0],(CHART_SZ-1)*sizeof(float));
  tempdat[0] = temp;
  humiddat[0] = humi;
}

Chart.jsで表示する履歴データを生成します。シリアル通信で取得したデータを配列の先頭に新しいデータが更新されるように格納します。

memmove()関数で配列の先頭から配列の最後尾よりも-1した範囲を配列の1つ分だけずらすことで配列の先頭を空けてから最新のデータを配列の先頭に格納します。

最新のデータがグラフの最後尾に表示する場合はChart.jsのオプションのX軸のスケールのreverseをtrueにすることでX軸のスケールの向きを反対にすることができます。

スポンサーリンク

子機の構成

子機の構成
子機の構成

TWELITEとTWE-EH-S及びSHT35を組み合わせた子機の構成です。TWELITEの電源にTWE-EH-Sに付属した太陽光パネルを使用しています。基本的な動作は下記記事の構成を流用しています。

トワイライト(TWELITE)の自作アプリとソーラーモジュールで無線通信

参考記事のモノスティックがTWELITE-DIPの親機に置き換わったイメージです。

TWELITEはI2C通信でSHT35から温湿度データを取得し、親機のTWELITEに無線送信します。下記記事にTWELITEでSHT35のデータを取得する方法をまとめています。

トワイライト(TWELITE)でSHT35のデータを取得する

14(12)ピンはGNDに接続した状態で子機の電源を入れるとインタラクティブモードに遷移します。子機の設定をトワイライターを使用せずに行う場合に使用します。トワイライターを使用する場合はMWSTAGEのインタラクティブモードを選択すると設定画面に遷移します。

MWSTAGEのインタラクティブモード画面
MWSTAGEのインタラクティブモード画面

アプリケーションID(Application ID)とチャンネル番号(Channel)を親機に合わせて設定します。子機の番号(Device ID)を区別する場合はデバイス番号を1~239を指定します。254は区別なしの子機になります。

PR:受講者20万人超!スマホで学べる人気のオンライン資格講座【STUDYing(スタディング)】

動作確認

ESP32のWiFiの範囲がTWELITEで範囲が拡張されていることを確認します。子機の電源は電気二重層コンデンサに3.3Vまで充電したものを使用しています。以下の手順で動作確認を行いました。

  1. 親機をシリアルモニターに接続する。
  2. 親機のアクセスポイントが検知できない距離をスマホで確認する。自宅を出るとすぐに検知ができなくなった。
  3. 検知できなくなった場所から少し離れた場所に子機を設置し、電源をONする。
  4. シリアルモニターで子機と通信できていることを確認する。
  5. スマホからアクセスポイントに接続し、履歴を確認する。

子機を設置してシリアルモニターを確認するとTWELITEから受信した温湿度データが表示されていました。

シリアルモニタの結果
シリアルモニタの結果

子機は6秒毎(タイムスタンプtsが6秒毎に更新)にスリープからウェイクアップし、温湿度データを親機に無線送信していることが確認できました。次にスマホでWiFiで温湿度データの履歴を確認しました。

温湿度情報のグラフ表示の結果
温湿度情報のグラフ表示の結果

WiFiのアクセスポイント「EngKapi1」を選択しパスワードに「11112222」を入力してアクセスポイントに接続します。

Google ChromeでIPアドレス「192.168.4.1」を指定するとESP32のWebサーバーが応答し、温湿度データの履歴が表示されることが確認できました。また、IPアドレス「192.168.4.1/ti」のように存在しないURLを指定するとFile Not Foundが表示されることも確認できました。

アクセスポイント、パスワード、IPアドレスはソースコードで変更している場合は指定したものと置き換えて接続してください。

WiFiがカバーできない範囲をZigBeeが補完することで遠隔地のセンサーの状態を管理することができるようになり応用範囲が広がります。

広告

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。

親機側:ESP32

#include <WiFi.h>
#include <SPIFFS.h>
#include <WebServer.h>

#define HEADER_1 'T'
#define HEADER_2 'W'
#define RING_SZ 64
#define OFFSET_SZ 4

#define TIME_UP 0
#define TIME_OFF -1
#define BASE_CNT 10 //10msがベースタイマとなる
#define TIM_RX_WAIT 5
#define TIM_RWE_WAIT 8000

#define CHART_SZ 600

const char *ssid = "EngKapi1"; //SSID
const char *pass = "11112222"; //password
const IPAddress ip(192,168,4,1); //IPアドレス
const IPAddress subnet(255,255,255,0); //サブネットマスク

typedef struct{
    uint8_t wp;
    uint8_t rp;
    uint8_t buf[RING_SZ];
}RING_MNG;

RING_MNG esp32Rcv;
uint8_t Rcvdata[RING_SZ];
uint32_t beforetimCnt = millis();
int16_t timRxWait = TIME_OFF;
int16_t timTweRcvWait = TIME_OFF;
float tempdat[CHART_SZ];
float humiddat[CHART_SZ];

WebServer Wserver(80);

/*** Local function prototypes */
void mainApp(void);
void mainTimer(void);
uint8_t CalcSum(uint8_t *buf, uint8_t sz );
void Rcvmain(void);
void ReadPointerAdd(void);
void Sht35Set(bool tweflg);
void HtmlSet(void);
void handleNotFound(void);

void setup() {

    Serial.begin(9600);
    Serial2.begin(115200);

    WiFi.softAP(ssid, pass); //WiFiのアクセスポイントの設定
    WiFi.softAPConfig(ip, ip, subnet); //アクセスポイントのIP及びサブネットマスク
    SPIFFS.begin();
    //Webサーバー
    Wserver.serveStatic("/myfavicon.ico",SPIFFS,"/myfavicon.ico");
    Wserver.serveStatic("/chart.umd.js",SPIFFS, "/chart.umd.js");
    Wserver.on("/", HTTP_GET, HtmlSet); //URLを指定して処理する関数を指定
    Wserver.onNotFound(handleNotFound); //URLが存在しない場合の処理する関数を指定
    Wserver.begin(); //Webサーバーの開始
}

void loop() {
  
  mainApp();
  mainTimer();
  Wserver.handleClient(); //クライアント接続待ち
}

/* メイン関数 */
void mainApp(void){

  while( Serial2.available()){
    esp32Rcv.buf[esp32Rcv.wp] = Serial2.read(); //データリード
    //Serial.write(esp32Rcv.buf[esp32Rcv.wp]);

    if( ++esp32Rcv.wp >= sizeof(esp32Rcv.buf)){
      esp32Rcv.wp = 0;
    }
  }

  Rcvmain();

  if( timTweRcvWait == TIME_UP ){
    Sht35Set(false);
  }

}

/* Timer Management function add */
void mainTimer(void){

  if ( millis() - beforetimCnt >= BASE_CNT ){
      beforetimCnt = millis();

        if( timRxWait > TIME_UP ){
            timRxWait--;
        }

        if( timTweRcvWait > TIME_UP ){
          timTweRcvWait--;
        }
    }
}

/* 受信データの処理 */
void Rcvmain(void){
  int8_t  rxsz;
  uint8_t datsz=0;
  uint8_t allsz;
  uint8_t rp = esp32Rcv.rp;  
  uint8_t i;
  uint8_t sumchk;
  uint8_t dat[3];

  if( timRxWait == TIME_UP){
    timRxWait = TIME_OFF;
    ReadPointerAdd();    
  }

  rxsz = esp32Rcv.wp - esp32Rcv.rp; //受信データ数の算出

  if( rxsz < 0 ){
    rxsz = rxsz + sizeof(esp32Rcv.buf);
  }          
  if( rxsz == 0 ){
    timRxWait = TIME_OFF;
  }
  else{
    if( timRxWait == TIME_OFF ){
      timRxWait = TIM_RX_WAIT;
    }

    if( esp32Rcv.buf[ rp ] != HEADER_1 ){ //ヘッダーの確認
      ReadPointerAdd(); 
    }
    else{
      if( rxsz >= 3 ){
        for( i = 0; i < 3; i++){//データサイズ算出のため仮おき
          dat[i] = esp32Rcv.buf[ rp ];
          if(++rp >= sizeof(esp32Rcv.buf) ) rp = 0;
        }

        if( dat[2] > sizeof(esp32Rcv.buf) - OFFSET_SZ ){
          allsz = sizeof(esp32Rcv.buf) - OFFSET_SZ;
        }
        else{
          datsz = dat[2];
          allsz = datsz + OFFSET_SZ; //header,sz,sumを含む
        }

        if( rxsz >= allsz){
          timRxWait = TIME_OFF;
                    
          for(i=0; i < sizeof(Rcvdata); i++){
            Rcvdata[i] = 0;
          }
                    
          for( i=0; i < allsz; i++ ){
            Rcvdata[i] = esp32Rcv.buf[esp32Rcv.rp];
            ReadPointerAdd();
          }

          sumchk = CalcSum(&Rcvdata[3], datsz); 
          if( sumchk == Rcvdata[allsz-1]){ //チェックサムが一致するか
            Sht35Set(true);
          }
        }
      }
    }
  }
}

/* 読み込み位置の更新 */
void ReadPointerAdd(void){
    
  if(++esp32Rcv.rp >= sizeof(esp32Rcv.buf) ){
    esp32Rcv.rp = 0;
  }
}
/* チェックサムの計算 */
uint8_t CalcSum(uint8_t *buf, uint8_t sz ){
  uint8_t ret = 0;

  for(uint8_t i=0; i < sz; i++ ){
    ret += *buf;
    buf++; 
  }
  return ret;
}
/* TWELiteから取得したデータをセット  */
void Sht35Set(bool tweflg){
    uint32_t tp=0;
    uint16_t temp=0;
    uint16_t humi=0;
    float temp_f;
    float humi_f;

    timTweRcvWait = TIM_RWE_WAIT;

    if( tweflg){
      tp = (Rcvdata[7] << 24) + (Rcvdata[6] << 16) + (Rcvdata[5] << 8) + Rcvdata[4];
      temp = (Rcvdata[9] << 8) + Rcvdata[8];
      humi = (Rcvdata[11] << 8) + Rcvdata[10];
    }
    temp_f = (float)temp/100;
    humi_f = (float)humi/100;

    Serial.print("tp:");
    Serial.print(tp);
    Serial.print(" temp:");
    Serial.print(temp_f);
    Serial.print("C");
    Serial.print(" humi:");
    Serial.print(humi_f);
    Serial.print("%");
    Serial.println();

    memmove( &tempdat[1], &tempdat[0],(CHART_SZ-1)*sizeof(float));
    memmove( &humiddat[1], &humiddat[0],(CHART_SZ-1)*sizeof(float));
    tempdat[0] = temp_f;
    humiddat[0] = humi_f;
}
/* グラフ表示 */
/* クライアントに返信するhtmlデータを生成 */
void HtmlSet(void){
  String str = "";

  str += "<html lang=\"ja\">";
  str += "<head>";
  //str += "<meta http-equiv=\"refresh\" content=\"5\">";
  str += "<meta charset=\"UTF-8\">";
  str += "<title>Sensor SHT35</title>";
  str += "<link rel='shortcut icon' href='/myfavicon.ico' />";
  str += "<script src = '/chart.umd.js'></script>"; 
  str += "</head>";
  str += "<body>";
  str += "<h1>ESP32-SHT35-DIS温湿度センサ</h1>";
  str += "<h2>TWELITEで取得したデータをWifeで確認</h2>";
  str += "<h2>温度: ";
  str += tempdat[0];
  str += "℃  ";
  str += "湿度: ";
  str += humiddat[0];
  str += "%RH";
  str += "</h2>";
  str += "<div style='height: 600px; width: 800px; margin: auto;'>";
  str += "<canvas id='ChartID'></canvas>";
  str += "</div>";
  str += "<script>";
  str += "var ctx = document.getElementById('ChartID').getContext('2d');";
  str += "var myChart = new Chart(ctx, {";
  str += "   type: 'line',";
  str += "   data: {";
  str += "     labels: [ ";
                 for(uint16_t i = 0; i< CHART_SZ; i++ ){
                   str += i; 
                   if(i != CHART_SZ-1 )str += ",";
                 }   
  str += "     ],";
  str += "       datasets: [{";
  str += "         label: '温度',";
  str += "         fill: false,";
  str += "         borderColor: 'red',";
  str += "         borderWidth: 1,";
  str += "         pointRadius: 0,";
  str += "         pointHoverBorderWidth: 10,";
  str += "         data: [";
                     for(uint16_t i = 0; i< CHART_SZ; i++ ){
                       str += tempdat[i]; 
                       if(i != CHART_SZ-1 )str += ",";
                     }
  str += "         ]";
  str += "       }, {";
  str += "         label: '湿度',";
  str += "         fill: false,";
  str += "         borderColor: 'blue',";
  str += "         borderWidth: 1,";
  str += "         pointRadius: 0,";
  str += "         pointHoverBorderWidth: 10,";
  str += "         pointStyle: 'triangle',";
  str += "         yAxisID: 'y2',";
  str += "         data: [";
                     for(uint16_t i = 0; i< CHART_SZ; i++ ){
                       str += humiddat[i]; 
                       if(i != CHART_SZ-1 )str += ",";
                     }
  str += "         ]";
  str += "       }]";
  str += "     },";
  str += "     options: {";
  str += "       responsive: true,";
  str += "       plugins: {";
  str += "         title: {";
  str += "           display: true,";
  str += "           text: 'SHT35-DISから取得した温度・湿度',";
  str += "           font: {";
  str += "             size: 18,";
  str += "           },";
  str += "         },";
  str += "       },";
  str += "       scales: {";
  str += "         x: {";
  str += "           display: true,";
  str += "           stacked: false,";
  str += "           title: {";
  str += "             display: true,";
  str += "             text: 'サンプル数',";
  str += "             font: {";
  str += "               size: 16,";
  str += "             },";      
  str += "           },";
  str += "         },";
  str += "         y: {";
  str += "           min: -20,";
  str += "           max: 80,";
  str += "           title: {";
  str += "             display: true,";
  str += "             text: '温度[℃]',";
  str += "             color: 'red',";
  str += "             font: {";
  str += "               size: 16,";
  str += "             },";
  str += "           },";
  str += "           ticks:{";
  str += "             display: true,";          
  str += "             color: 'red',";
  str += "           },";
  str += "         },";
  str += "         y2: {";
  str += "           min: 0,";
  str += "           max: 100,";
  str += "           title: {";
  str += "             display: true,";
  str += "             text: '湿度[%]',";
  str += "             color: 'blue',";
  str += "             font: {";
  str += "               size: 16,";
  str += "             },";
  str += "           },";
  str += "           position: 'right',";
  str += "           ticks:{";
  str += "             display: true,";
  str += "             color: 'blue',";
  str += "           },";
  str += "         },";
  str += "       },";
  str += "     },";
  str += "})";
  str += "</script>";
  str += "</body>";
  str += "</html>";

  Wserver.send(200,"text/html", str); 
  //HTTPレスポンス200でhtmlデータとして送信
}

/* URLが存在しない場合の処理 */
void handleNotFound(void) {

  String message = "File Not Found\n\n";
  message += "URI: ";
  message += Wserver.uri();
  message += "\nMethod: ";
  message += (Wserver.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += Wserver.args();
  message += "\n";

  for (uint8_t i = 0; i < Wserver.args(); i++) {
    message += " " + Wserver.argName(i) + ": " + Wserver.arg(i) + "\n";
  }
  Wserver.send(404, "text/plain", message); //テキストファイルであることを示している。
}

本ソースコードでグラフ表示するためにChart.jsライブラリをESP32 Sketch Data Uploadまたは下記の参考記事による方法でFLASHにファイルを書き込む必要があります。

ESP32-WROOM-32EにSDカードのファイルをアップロードする

またChart.jsのバージョンが4.0.0以上ならchart.umd.js(旧バージョンではchart.min.js)を使用します。

親機側:TWELITE(参考記事:Arduinoとトワイライトの組み合わせによる親機のリンク)

#include <TWELITE>
#include <NWK_SIMPLE>

#define CNT_MAX 1000
#define HEADER1 'T'
#define HEADER2 'W'

const uint32_t APP_ID = 0x1234cdef;
const uint8_t CHANNEL = 13;

struct TXCOM_TYP{
  uint8_t header[2];
  uint8_t sz;
  uint8_t cnl;
  uint8_t ts[4];
  uint8_t temp[2];
  uint8_t humi[2];  
  uint8_t sum;
};

int16_t cnt1ms;
uint32_t ts;
bool rcvflg;
TXCOM_TYP txdata;

/*** function prototype */
void receive(void);
void SerialTxSet(void);
uint8_t CalcSum(uint8_t *buf, uint8_t sz);

/*** setup procedure (run once at cold boot) */
void setup() {

  the_twelite
    << TWENET::appid(APP_ID)
    << TWENET::channel(CHANNEL)
    << TWENET::rx_when_idle();

  // Register Network
  auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
  nwksmpl << NWK_SIMPLE::logical_id(0x00);
  the_twelite.begin();

  Serial << "--- Parent_sht35 act ---" << mwx::crlf;
}

/*** loop procedure (called every event) */
void loop() {

  if(TickTimer.available()){
    ++cnt1ms;
  }

  while (the_twelite.receiver.available()) {
    receive();
    rcvflg = true;
  }

  if( cnt1ms >= CNT_MAX){
    cnt1ms -= CNT_MAX;

    ts = (ts + 1 ) % 40000000;

    if(rcvflg){
      rcvflg = false;
    }else
    {
      Serial << "ts:" <<  int(ts) << mwx::crlf << mwx::flush;;
    }
  }
  }
/* 受信データをシリアル通信する */
void receive(void) {
  uint16_t temp;
  uint16_t humi;

  auto&& rx = the_twelite.receiver.read();
  //Serial << format("..receive(%08x/%d) : ", rx.get_addr_src_long(), rx.get_addr_src_lid());
  Serial << "ts:" << int(ts) << ":" << int(rx.get_addr_src_lid());

  auto&& np = expand_bytes(rx.get_payload().begin(),rx.get_payload().end()
        ,temp
        ,humi
  );

  txdata.ts[0] = ts & 0xFF;
  txdata.ts[1] = ts >> 8;
  txdata.ts[2] = ts >> 16;
  txdata.ts[3] = ts >> 24;
  txdata.cnl = int(rx.get_addr_src_lid());
  txdata.temp[0] = temp & 0xFF;
  txdata.temp[1] = temp >> 8;
  txdata.humi[0] = humi & 0xFF;
  txdata.humi[1] = humi >> 8;
  SerialTxSet();

  //Serial << " temp: " << (double)temp/100 <<"C" 
  //       << " humid: " << (double)humi/100 << "%" 
  //	   << mwx::crlf << mwx::flush;
}

/* TX function add */
void SerialTxSet(void){
  uint8_t *adrs;
  uint8_t allsz = 0;
  uint8_t i;

  txdata.header[0] = HEADER1;
  txdata.header[1] = HEADER2;
  txdata.sz = sizeof(txdata.cnl) + sizeof(txdata.ts) + 
              sizeof(txdata.temp) + sizeof(txdata.humi);
  txdata.sum = CalcSum(&txdata.cnl,txdata.sz);
  allsz = sizeof(txdata.header)+ sizeof(txdata.sz)+ txdata.sz + 1;

  adrs =&txdata.header[0];
  for(uint8_t i = 0; i < allsz; i++ ){
    Serial << (*adrs);
    adrs++;
  }
}

/* ChkSum function add */
uint8_t CalcSum(uint8_t *buf, uint8_t sz=0){
  uint8_t ret = 0;

  for(uint8_t i=0; i < sz; i++ ){
    ret = ret + (*buf);
    buf++; 
  }
  return ret;
}

子機:TWELITE(参考記事:トワイライトの子機のリンク)

#include <TWELITE>
#include <NWK_SIMPLE>
#include <SNS_SHT3X>
#include <STG_STD>

#define TIME_UP 0
#define TIME_OFF -1
#define TIME_OUT_MAX 100
#define SLEEP_DUR 600000
#define AD_INIT_VALUE 0xFFFF
#define AD_VCC_CHK 2500 //2.5V

const uint8_t PIN_DO1 = mwx::PIN_DIGITAL::DIO18;    //Boot
const uint8_t PIN_DO2 = mwx::PIN_DIGITAL::DIO19;    //Byp
const uint8_t PIN_AD1 = mwx::PIN_ANALOGUE::A1;		//TWE-EH-SのVCC

typedef union{
  uint8_t byte[6];
  struct{
    uint32_t app_di;
    uint8_t channel;
    uint8_t logical_id;       
  }dat;
}SYSTEM_TYP;

typedef struct{
  uint16_t temp;
  uint16_t humi;
}SENSOR_TYP;

enum  E_STATE {
  INIT = 0,
  WORK_JOB,
  WORK_JOB2,
  TX,
  WAIT_TX,
  EXIT_NORMAL,
  EXIT_FATAL
};

enum  APP_MODE{
  INTERACTIVE = 0,
  NORMAL
};

/* 変数宣言 */
APP_MODE appmode = APP_MODE::NORMAL;
SNS_SHT3X sns_sht3x;
MWX_APIRET txreq_stat;
SYSTEM_TYP twesystem;
uint32_t u32millis_tx; // millis() at Tx
SENSOR_TYP getsensor;
E_STATE eState;
bool b_found_sht3x = false;
bool b_senser_started = false;
bool b_transmit = false;
bool vc2Dosetflg = false;
int16_t timTxTimeout = TIME_OFF;
uint16_t vc2 = AD_INIT_VALUE;

/* プロトタイプ宣言 */
void mainTimer(void);
MWX_APIRET transmit(void);
void napNow(void);
void sleepNow(void);

/*** the setup procedure (called on boot) */
void setup() {

  Serial.begin();

  auto&& set = the_twelite.settings.use<STG_STD>();
  set << SETTINGS::appname("MY_APP"); //アプリ名
  set << SETTINGS::appid_default(0x1234cdef); //デフォルトID

  pinMode(PIN_DIGITAL::DIO12, PIN_MODE::INPUT_PULLUP);
  if (digitalRead(PIN_DIGITAL::DIO12) == LOW) {
    set << SETTINGS::open_at_start(); //インタラクティブモード起動
    appmode = APP_MODE::INTERACTIVE; //loop()で動作しないようにするため                               
  }
  else{
    appmode = APP_MODE::NORMAL;
    pinMode(PIN_DO1, OUTPUT_INIT_LOW); //BOOTをLOW
    pinMode(PIN_DO2, OUTPUT_INIT_HIGH); //BYPをHIGH

    Analogue.setup(true, ANALOGUE::KICK_BY_TICKTIMER);
    Analogue.begin(pack_bits(PIN_AD1,PIN_ANALOGUE::VCC),5);

    Wire.begin();
    sns_sht3x.setup(0x45);
    if(sns_sht3x.probe()){
      b_found_sht3x = true;
      Serial << "sht3x-ok" << crlf << mwx::flush;
    }

    set.reload();
    twesystem.dat.app_di = set.u32appid();
    twesystem.dat.channel = set.u8ch();
    twesystem.dat.logical_id = set.u8devid();

    txreq_stat = MWX_APIRET(false, 0);
    the_twelite
      << TWENET::appid(twesystem.dat.app_di)
      << TWENET::channel(twesystem.dat.channel)
      << TWENET::tx_power(0x03);

    auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
    nwksmpl << NWK_SIMPLE::logical_id(twesystem.dat.logical_id);
    the_twelite.begin();
  }
}

/*** the loop procedure (called every event) */
void loop() {
  bool loop_more;

  switch (appmode){
    case APP_MODE::INTERACTIVE:
      //インタラクティブモード
      break;
    case APP_MODE::NORMAL:
      do{
        loop_more = false;

        switch(eState){
          case E_STATE::INIT: //デバイスの確認
            sns_sht3x.setup(0x45);
            if(sns_sht3x.probe()){
              b_found_sht3x = true;
              loop_more = true;
              eState = E_STATE::EXIT_NORMAL;
              Serial << "sht3x-ok" << crlf << mwx::flush;
            }
            else{
              sleepNow();
            }
            break;
          case E_STATE::WORK_JOB: //経過時間の通知
            if (!sns_sht3x.available()) {
              sns_sht3x.process_ev(1);
            }
            loop_more = true;
            eState =  E_STATE::WORK_JOB2;
            break;
          case E_STATE::WORK_JOB2: //測定データの取得
            if(sns_sht3x.available()){
              timTxTimeout = TIME_OFF;  
              getsensor.temp = sns_sht3x.get_temp_cent();
              getsensor.humi = sns_sht3x.get_humid_per_dmil();

              Serial 
                << crlf << format("..%04d/finish sensor capture.", millis() & 8191)
                << crlf << "  SHT3X: T=" << sns_sht3x.get_temp() << 'C'
                << " H=" << sns_sht3x.get_humid() << '%' << crlf;
              eState =  E_STATE::TX;
              loop_more = true;
            }
            break;
          case E_STATE::TX:
            txreq_stat = transmit(); //データの取得準備
            if (txreq_stat) {
              timTxTimeout = TIME_OUT_MAX;
              eState = E_STATE::WAIT_TX;
              loop_more = true;
            }else {
              eState = E_STATE::EXIT_FATAL;
              loop_more = true;
            }
            break;
          case E_STATE::WAIT_TX:
            if (the_twelite.tx_status.is_complete(txreq_stat.get_value())) { //送信完了ステータス待ち
                //Serial << int(millis()) << ":tx completed! (" << int(txreq_stat.get_value()) << ')' << crlf;
                eState = E_STATE::EXIT_NORMAL;
            }else if (timTxTimeout == TIME_UP) {
              Serial << int(millis()) << "!FATAL: tx timeout." << crlf;
              eState = E_STATE::EXIT_FATAL;
              loop_more = true;
            }
            break;
          case E_STATE::EXIT_NORMAL:
            timTxTimeout = TIME_OFF; 
            sleepNow();
            break;
          case E_STATE::EXIT_FATAL:
            Serial << crlf << "!FATAL: RESET THE SYSTEM.";
            delay(100);
            the_twelite.reset_system(); //異常とみなしリセット
            break;
        }
      }while(loop_more);

      mainTimer();
      break;  
  }

}
/* タイマ管理処理 */
void mainTimer(void){

  if( TickTimer.available()){
    if( timTxTimeout > TIME_UP ){
      --timTxTimeout;
    }
  }
}

/* callback begin */
void begin(){

  if(appmode != APP_MODE::INTERACTIVE){
    sleepNow();
  }
}
/* callback wakeup */
void wakeup(){

  if (b_senser_started) { //napからwakeup
    if( b_found_sht3x ){
      eState = E_STATE::WORK_JOB;
      timTxTimeout = TIME_OUT_MAX;
    }
    else{
      eState = E_STATE::INIT;
    }

    b_transmit = false;
    //Serial << "nap-wakeup" << crlf << mwx::flush;
  }
  else{ //sleepからwakeup
    b_senser_started = true;
    sns_sht3x.begin();
    //Serial << "sleep-wakeup" << crlf << mwx::flush;
    napNow();
  }
}
/* ナップ(ショートスリープ)処理 */
void napNow(){
  uint32_t u32ct = 100;

  the_twelite.sleep(u32ct, false, false, TWENET::SLEEP_WAKETIMER_SECONDARY);
}
/* スリープ処理 */
void sleepNow(){
  uint32_t u32ct = SLEEP_DUR + random(0,200);

  if( vc2Dosetflg == false){
    if( vc2 >= AD_VCC_CHK && vc2 != AD_INIT_VALUE){
      vc2Dosetflg = true;
      pinMode(PIN_DO2, OUTPUT_INIT_LOW); //BYPをLOW
    }
  }

  b_senser_started = false;
  b_transmit = false;
  the_twelite.sleep(u32ct);
}
/* 送信パケット準備 */
MWX_APIRET transmit() {

  if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
    pkt << tx_addr(0x00)  // 0..0xFF (0:parent, FE:child)
      << tx_retry(0x2) // set retry (0x1 send two times in total)
      << tx_process_immediate();
      
      pack_bytes(pkt.get_payload(),getsensor.temp, getsensor.humi);
    return pkt.transmit();
  }
  return MWX_APIRET(false, 0);
}

子機のTWELITEの無線送信のタイミングは動作確認では6秒にしていますが、実用性を考慮して600秒にしています。

TWELITEの開発環境であるMWSTAGEのアクトを使用してTWELITE-DIPにソースコードを書き込むことで動作します。

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

TWELITEを使ってソフト開発したことについてまとめています。

トワイライト(TWELITE)のソフト開発と無線通信でできること

広告

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました