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

組み込みエンジニア

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

ZigBeeモジュールであるトワイライト(TWELITE)を使用し、各種センサーと組み合わせることでIoTへの応用が期待できます。トワイライトの子機からデータを無線受信する親機を実装しシリアルモニターに表示する方法についてまとめました。

TWELITE DIPを2つ使用し一方を親機もう一方を子機として使用します。子機には下記記事に使用したソフトを流用して使用します。

トワイライト(TWELITE)にAD変換と無線通信を実装する

トワイライトを太陽光パネルで動作させたことやMWSTAGEの環境でソフト開発して無線通信したことなどについてまとめています。

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

親機を実装する

親機は子機からの無線通信を受信してシリアル通信でモニターできるようにします。親機は同一のアプリケーションIDやセットとなる文字列4文字(任意でよい)によってセットとなる子機からの電文であることの判断を行います。

ACTの「BRD_APPTWELITE」を参考にしながら親機としての機能を実装していきます。具体的には子機からの無線データを受信してシリアルでデータを書き出します。BRD_APPTWELITEの詳細の説明は下記のリンクを参考にしてください。

モノワイヤレス社ーBRD_APPTWELITE

const uint32_t APP_ID = 0x1234abcd;
const uint8_t CHANNEL = 13;
const char APP_FOURCHAR[] = "TEMP";

APP_IDとチャンネルはサンプルと同じにしています。APP_FOURCHAR[]は受信したデータに自作のアプリに埋め込んだアプリ識別のための文字列です。

子機側のソフトにおいてもAPP_FOURCHAR[]と同じ文字列を送信して親機が受信して文字列を比較して一致した場合にデータを受け付けるような処理に使用します。

the_twelite
    << TWENET::appid(APP_ID)    //アプリケーションID
    << TWENET::channel(CHANNEL) //チャンネル設定
    << TWENET::rx_when_idle();  //無線データを受信するために必要


auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
       nwksmpl << NWK_SIMPLE::logical_id(0x00); //0x00にすると親機設定(子機は0xFE)

the_tweliteにおいてrx_when_idle()を設定しておく必要があります。子機として送信専用で使用する場合は不要でしたが、無線データを受信する必要があるため実装します。ネットワークのlogical_idに0x00をセットすることで親機として動作します。

メイン関数(loop())内では受信待ちの処理でループするように構成します。

void loop() {

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

    while (the_twelite.receiver.available()) {
	receive(); //受信したらこの関数で処理
	rcvflg = true;
    }

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

	ts = (ts + 1 ) % 100000; //1秒に一回更新

	if(rcvflg){
	    rcvflg = false;
	}
        else{
	    Serial << "ts:" <<  int(ts) << mwx::crlf << mwx::flush;;
	}
    }
}

受信待ちはthe_twelite.receiver.available()で行います。受信したデータが残っていると直ちに処理するためにwhile文でパケット処理が終了するまで受信処理を行います。

データが受信するまでの期間に何も表示されないと動作しているかわかりにくいためタイムスタンプとしてtsをカウントしながらシリアルで書き込み表示しています。

tsは5桁の数値で収まるように%で余りの数としてカウントするようにしています。これは以下のように書き換えることができます。

if(++ts >= 100000){
    ts = 0;
}

受信したときのループを抜けた先にタイムスタンプの表示のためシリアル書き込みがありますが、受信処理の中でシリアル書き込みを行うため2回ダブってタイプスタンプを書かないようにフラグ管理しています。

フラグ管理していても1秒以内に処理が終わる場合同じ時刻がタイプスタンプとして表示されてしまいますが、子機が複数台になったときは同時刻のタイムスタンプがありえるため許容できる範囲だと思います。

void receive() {

    auto&& rx = the_twelite.receiver.read();
    char fourchars[5]{}; // init all elements as default (0).
    auto&& np = expand_bytes(rx.get_payload().begin(), rx.get_payload().end()
	        , make_pair((uint8_t*)fourchars, 4)  // 4bytes of msg
                );

    if (strncmp(APP_FOURCHAR, fourchars, 4)) {return; } //子機の文字ヘッダーが一致するか
    expand_bytes(np,rx.get_payload().end(),adrs,vcc,temp);
}

expand_bytes()は受信したペイロード(データのパケット)を指定したバイト数に分けて受信できるため使い勝手の良いAPIです。最初に4バイト受信しているのはアプリの文字列が一致するかのチェックを行うためです。

strncmp()で親機と子機の文字列が一致した場合は同一アプリのものと判断して文字列より先のペイロードのデータを取得する処理を行います。npはポインタで読み込んだデータ分より次のデータの位置を示すポインタになっています。

これを利用して文字列以降のデータのチェックサムを行ったりデータ長のチェックを行ったりできそうです。例えば固定長としexpand_baytes()に指定する1バイト分のデータの配列を準備しておき規定回数繰り返すことでデータを取得してチェックサムの計算を行い比較できそうです。

for( i = 0; i < 10; i++){
    expand_bytes(np,rx.get_payload().end(),buf[i]);
   //チェックサムの計算を入れる
}

今回はチェックサムによるデータの確認は行いませんが、実装するとしたら上記のようなイメージになると思います。

子機を準備する

子機は下記記事で使用したソフトを流用します。

トワイライト(TWELITE)にAD変換と無線通信を実装する

変更点は消費電流を抑えるためにショートスリープを実装したことや子機のアドレスを区別するためにDIを2点追加したことです。

pinMode(PIN_DO1, OUTPUT_INIT_LOW); //BOOTをLOW
pinMode(PIN_DO2, OUTPUT_INIT_HIGH); //TEMP OFF
pinMode(PIN_DI1,INPUT_PULLUP);//アドレス用
pinMode(PIN_DI2,INPUT_PULLUP);//アドレス用

PIN_DO2は温度センサーへの電源供給を制御するために使用します。温度センサー(MCP9700A)に電源を供給し続けるとその分消費電流が増えるためAD変換する100ms前に温度センサーの電源をONするために準備しています。

void wakeup(){

    if (!b_senser_started) { //スリープからのウェークアップ
        b_senser_started = true;
	digitalWrite(PIN_DO2,LOW);//センサーをON
        napNow();//ショートナップ(100msスリープ)
    }
    else{
        b_transmit = false;
    }
}

スリープからウェークアップしたときに温度センサーへの電源供給を行いショートナップ(100ms)をすることで温度センサーのデータが取得できる状態とします。DOにシンク電流を引き込むことになりますが、温度センサーの消費電流が数uAなので問題ありません。

動作確認

子機を2台準備しアドレスに変化を与えて親機に無線通信して温度情報を表示します。

動作確認用の回路図

子機の回路図
子機の回路図

電源はTWE-EH-Sを使用します。子機は電源電圧とMCP9700A(温度)のデータをAD変換して無線通信します。今回は5秒間欠動作にしていますが、TWE-EH-Sに接続したバックアップ電源の消耗を抑えるために間欠動作のタイミングを長く(5分に一度)した方が良いかもしれません。

子機はアドレスを1と2で区別して親機にデータを送信したいので2台準備します。1台のDI2をGND接続しもう一台のDI1をGNDに接続することでアドレスを区別します。

動作結果

親機の表示:子機2台からの受信データ
親機の表示:子機2台からの受信データ

親機はトワイライターに接続したままTeraTermで表示します。MWSTAGE上でモニターできますが文字化けして表示できない現象があったためTeraTermにしています。

親機にアドレス1とアドレス2からのデータが受信できておりデータが表示されています。タイムスタンプ後の254は子機のIDである0xFEが数値化したものです。VCCは子機のトワイライトの電源をAD変換したものになるため電圧をモニタすることで電池の消耗具合が分かります。

シリアルモニタのVcc:の表示の桁を間違えていました。正確には3.042Vや3.158Vなどです。ソースコードでは修正しています。

tempは温度センサーのデータをMCP7800Aのデータシートに基づいて温度換算した値を表示しています。天気が悪く室内環境は冷房環境であり室温系を見た時24.5℃だったため計測できていることが分かりました。アドレス2の方が値が高くなっているのは窓際に配置したためです。(アドレス1は作業デスク上に配置)

ソースコード全体

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

親機のソースコード:

// use twelite mwx c++ template library
#include <TWELITE>
#include <NWK_SIMPLE>

#define CNT_MAX 1000

/*** Config part */
const uint32_t APP_ID = 0x1234abcd;
const uint8_t CHANNEL = 13;
const uint8_t PIN_DI1 = mwx::PIN_DIGITAL::DIO10; 
const uint8_t PIN_DO1 = mwx::PIN_DIGITAL::DIO9; 

// application use
int16_t cnt1ms;
uint32_t ts;
bool rcvflg;
const char APP_FOURCHAR[] = "TEMP";
uint8_t u8devid = 0;

/*** function prototype */
void receive();

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

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

    auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
	   nwksmpl << NWK_SIMPLE::logical_id(0x00); // set Logical ID. (0x00 means parent device)

    the_twelite.begin(); // start twelite!
    Serial << "--- Parent 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 ) % 100000;

	if(rcvflg){
	    rcvflg = false;
	}else{
	    Serial << "ts:" <<  int(ts) << mwx::crlf << mwx::flush;;
	}
    }
}
/*add function recive()*/
void receive() {
    uint8_t adrs;
    uint16_t vcc;
    uint16_t temp;
    double temp2;
    double vcc2;
   
    auto&& rx = the_twelite.receiver.read();
    char fourchars[5]{}; // init all elements as default (0).
    auto&& np = expand_bytes(rx.get_payload().begin(), rx.get_payload().end()
		, make_pair((uint8_t*)fourchars, 4)  // 4bytes of msg
                );

    if (strncmp(APP_FOURCHAR, fourchars, 4)) {return; } //文字列チェック

    expand_bytes(np,rx.get_payload().end(),adrs,vcc,temp);
    temp2 = (double)( temp - 500 ) / 10; //電圧値から温度換算
    vcc2 = (double)vcc/1000; //電圧値から電源電圧換算
    Serial << "ts:" << int(ts) << ":" << int(rx.get_addr_src_lid())
	   << " adrs: " << (int)adrs << " vcc: " << vcc2 << "V"
           << " temp: " << temp2 << "deg" << mwx::crlf << mwx::flush;
}

子機のソースコード:

#include <TWELITE>
#include <NWK_SIMPLE>

#define TIME_UP 0
#define TIME_OFF -1
#define ZIG_WAIT_MAX 100
#define ADC_FLT_MAX 4

/*** Config part */
const uint32_t APP_ID = 0x1234abcd; // application ID
const uint8_t CHANNEL = 13; // channel
/*** application defs */
const char APP_FOURCHAR[] = "TEMP";

struct ADC_MEAN
{
    uint8_t wp;
    uint16_t buf[ADC_FLT_MAX];
    uint16_t dat;
};

const uint8_t PIN_DO1 = mwx::PIN_DIGITAL::DIO18; //Boot
const uint8_t PIN_DO2 = mwx::PIN_DIGITAL::DIO9; //TEMP
const uint8_t PIN_DI1= mwx::PIN_DIGITAL::DIO12; //adrs1
const uint8_t PIN_DI2= mwx::PIN_DIGITAL::DIO13; //adrs2
const uint8_t PIN_AD1 = mwx::PIN_ANALOGUE::A1; //温度
// application use
MWX_APIRET txreq_stat;
uint16_t au16AI[2];
ADC_MEAN adTemp;
bool b_transmit = false;
bool ad_finish = false;
uint8_t u8devid = 0x00;	//parent
bool b_senser_started;
int16_t timzigwait = TIME_OFF;
uint8_t adrs;

/*** Local function prototypes */
void sleepNow();
void napNow();
MWX_APIRET transmit();

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

    pinMode(PIN_DO1, OUTPUT_INIT_LOW); //BOOTをLOW
    pinMode(PIN_DO2, OUTPUT_INIT_HIGH); //TEMP OFF
    pinMode(PIN_DI1,INPUT_PULLUP);
    pinMode(PIN_DI2,INPUT_PULLUP);

    delay(10);
    adrs = digitalRead(PIN_DI2) << 1;//子機のアドレスを計算
    adrs = adrs + digitalRead(PIN_DI1); //子機のアドレスを計算

    for(auto&& x : au16AI) x = 0xFFFF;
    for(auto&& x : adTemp.buf) x = 0xFFFF;

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

    txreq_stat = MWX_APIRET(false,0);
    the_twelite
	<< TWENET::appid(APP_ID)    
	<< TWENET::channel(CHANNEL) 
	<< TWENET::rx_when_idle();      

    auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
	   nwksmpl << NWK_SIMPLE::logical_id(0xFE); //子機をセット  
    the_twelite.begin();
    //Serial << "--- wakup-com->start ---" << mwx::crlf;
}
/*** the loop procedure (called every event) */
void loop() {

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

    if ( b_transmit ) { //送信受け付け完了したか
	if (the_twelite.tx_status.is_complete(txreq_stat.get_value())) { //送信が完了したか	
	    //Serial << "..transmit complete." << mwx::crlf << mwx::flush;
	    timzigwait = TIME_OFF;
	    sleepNow();
	}
        else{
	    if(timzigwait == TIME_UP){
	        timzigwait = TIME_OFF;
		sleepNow();
	    }
        }
    }
    else{
	txreq_stat = transmit();
	    if(txreq_stat){
	        //Serial << "..sleep2." << mwx::crlf << mwx::flush;
		b_transmit = true;
		timzigwait = ZIG_WAIT_MAX;
            }
            else{
		Serial << "..chk2." << mwx::crlf << mwx::flush;
		sleepNow();
	    }
	}
	
    if(Analogue.available()){
	au16AI[0] = Analogue.read(PIN_ANALOGUE::VCC); //VCCの値
	au16AI[1] = Analogue.read(PIN_AD1); //温度センサーの値
	adTemp.buf[adTemp.wp] = Analogue.read(PIN_AD1); //温度センサーの値を格納

	uint16_t sum;
	ad_finish = true;

	for( uint8_t i=0; i < ADC_FLT_MAX; i++ ){
	    if( adTemp.buf[i] == 0xFFFF){
	        ad_finish = false;
		break;
	    }else{
		sum += adTemp.buf[i]; //合計を算出
	    }
	}

        if( ad_finish ){
	    adTemp.dat = sum >> 2; //4で割る(2回右にシフトする)
	}

	if( ++adTemp.wp >= ADC_FLT_MAX){ //値の格納場所を更新する
	    adTemp.wp = 0;
	}
    }
}
/* callback wakeup */
void wakeup(){

    if (!b_senser_started) { //スリープからのウェークアップ
        b_senser_started = true;
	digitalWrite(PIN_DO2,LOW);
        napNow();
    }
    else{
        b_transmit = false;
    }
}
// perform sleeping
void sleepNow() {
    uint32_t u32ct = 4900 + random(200); //5000ms+ランダム間スリープ
    //Serial << int(adrs) << "..sleeping " << int(u32ct) << "ms." << mwx::crlf << mwx::flush;
    b_senser_started = false;
    digitalWrite(PIN_DO2,HIGH); //温度センサーをOFF
    the_twelite.sleep(u32ct);
}
/* perform short period sleep */
void napNow() {
    uint32_t u32ct = 100;
    //Serial << "..nap " << int(u32ct) << "ms." << mwx::crlf << mwx::flush;
    the_twelite.sleep(u32ct, false, false, TWENET::SLEEP_WAKETIMER_SECONDARY);
}
/*** transmit a packet */
MWX_APIRET transmit() {

    if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
        //Serial << "analogue"<< int(au16AI[0]) << ".."<< int(adTemp.dat)   << mwx::crlf << mwx::flush;
	pkt << tx_addr(u8devid)  
	    << tx_retry(0x1) 
	    << tx_packet_delay(0,0,2);

	pack_bytes(pkt.get_payload(), make_pair(APP_FOURCHAR, 4),adrs, au16AI[0], adTemp.dat);
        return pkt.transmit();
    }
	return MWX_APIRET(false, 0);
}

動作開始直前の数回は温度の値が安定しません。平均化しているため4回分のデータが揃わなければ平均値がバラつくからです。温度データが安定してから送信する仕組みを実装してもよさそうです。

関連リンク

トワイライトを太陽光パネルで動作させたことやMWSTAGEの環境でソフト開発して無線通信したことなどについてまとめています。

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

GEEKJOB-未経験からITエンジニアに【オンライン無料体験】

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

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