トワイライト(TWELITE)で土壌の状態を取得する

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

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

ZigBeeモジュールであるトワイライト(TWELITE)のAD変換を使って土壌センサーの出力電圧を取得することができます。土壌センサーのSEN0114から取得したデータを無線通信してMONOSTICKでデータを表示する方法をまとめました。

無線通信する方法は下記記事の構成を流用しています。

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

土壌センサーはSEN0114(DFROBOT製:秋月電子で購入)を使用しています。トワイライトを太陽光パネルで動作させたことやMWSTAGEの環境でソフト開発して無線通信したことなどについてまとめています。

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

土壌センサーの情報を取得する

SEN0114は土壌の抵抗分で発生する電圧を出力するセンサーです。水分量が多いほど土壌の抵抗分が少なくなり電気を通しやすくなることを利用したものです。下記リンクにセンサーの情報とスケッチ例が説明されています。

SEN0114 Moisture Sensor-DFROBOT

Arduino環境で使用できるように出力調整されており土壌の状態の目安が記載されています。DC3.3VでのSEN0114の動作検証は下記記事のものを参考にします。

Seeeduino XIAOで土壌センサーの情報を取得

以下ではDC5VやDC3.3VをDCなしで記載します。

広告

動作検証(動作基準値の調整)

SEN0114の動作検証

トワイライトは3.3V動作なので3.3V時の動作の目安について検証を行いました。Arduino(5V)から取得した値で土壌の状態の目安として0~300 dry soil、300~700 humid soil、700~950 in waterと記載されています。

トワイライトのスリープ機能を使用して間欠動作させることを前提とするためスリープを使用した状態でSEN0114からデータを取得して動作検証を行います。

SEN0114の消費電流を押さえるためにトワイライトがウェークアップするタイミングの前後のみSEN0114の電源がONするようにします。

DOピンをSEN0114のGNDに接続してLOWレベルにすると出力が得られますが、SEN0114の消費電流は35mA(@5V)となっているためトワイライトのDOピンにシンク電流として取り込む際は注意が必要です。(詳細は下の自作アプリとTWE-EH-Sの全体構成を参照)

in waterの場合の値をArduino(5V)の基準に合わせるためSEN0114の出力電圧を可変抵抗で分圧した電圧をトワイライトのA2に入力して調整を行います。

AD変換値の確認(rawの値が750付近になるように調整)
AD変換値の確認(rawの値が750付近になるように調整)

Arduino(5V)で水につけた場合は750程度の値になるのでトワイライトのモニタでAD変換値を確認しながら電圧の調整を行います。rawにAD変換値を電圧値に換算する前のでデジット値を示しています。この値が750付近の値を持つように可変抵抗で調整します。

Arduino環境の場合でも5.0Vと3.3Vの場合でin water時の値に差がみられたことから目安程度の調整になります。

広告

自作アプリとTWE-EH-Sの全体構成

自作アプリのTWELITEとTWE-EH-Sの全体構成
自作アプリのTWELITEとTWE-EH-Sの全体構成

電源は太陽光パネルを利用したTWE-EH-Sを使用します。スリープ機能を使った間欠動作の場合条件によっては電池無しでも動作させることができます。

トワイライトの消費電流を減少させると同時に土壌センサーのSEN0114が消費電流についても減少させることが必要です。トワイライトがスリープしている間はSEN0114の電源をOFFして消費電流を減少させます。

SEN0114の消費電流は35mA(@5V)となっているためトワイライトのDOに直接シンク電流として取り込むのは望ましくありません。トランジスタをスイッチングで使用してトワイライトに電流が流れ込まないようにします。

TWE-EH-SのBOOTはトワイライトの5ピンのDOによって制御します。BOOTをLOWにするとリセットがかからなくなるためトワイライトへの電源供給が続きます。BYPは8ピンのDOをLOWにすると余剰電力のコンデンサC2をバイパスします。(動作の説明の詳細はトワイライト(TWELITE)の自作アプリとソーラーモジュールで無線通信を参照)

C1は無線タグアプリの場合は220uFで十分ですが自作アプリで処理を追加している分消費電流が増えてしまい電圧降下が大きくなり起動が安定しないことがあるため1000uFを追加し初期起動時の電圧降下の対策を行っています。

C2は余剰電力をチャージできるため容量の大きな電気二重層コンデンサなどを実装すると効果的です。2.2Fの電気二重層コンデンサでも曇り空でわずかに日が差す程度の環境下において1時間程度でフル充電できます。

BYPを初期起動時にLOWにするとC2が十分にチャージできていない場合C1にチャージした電荷がC2側に電荷が移ってしまうため電圧低下によりTWELITEのBOOTの制御が維持できなくなりTWE-EH-Sがリセット状態となります。

初期化時にはBYPを使用せず一定電圧以上になったことを確認してバイパスと効果的です。

const uint8_t PIN_DO1 = mwx::PIN_DIGITAL::DIO18;    //Boot
const uint8_t PIN_DO2 = mwx::PIN_DIGITAL::DIO19;    //Byp

void setup(){
    pinMode(PIN_DO1, OUTPUT_INIT_LOW); //BOOTをLOW
    pinMode(PIN_DO2, OUTPUT_INIT_HIGH); //BYPをHIGH
}
//BYPを使用する例
    if( vc2Dosetflg == false){
        if( vc2 >= AD_VCC_CHK && vc2 != AD_INIT_VALUE){
            vc2Dosetflg = true;
            pinMode(PIN_DO2, OUTPUT_INIT_LOW); //BYPをLOW
        }
    }

例ではVC2がAD_VCC_CHK(2400:2.4V相当)以上かつ初期化時の値でない場合にBYPをLOWにしてバイパスしています。余剰電力の充電状況を確認するためVC2をA1に接続しています。

広告

スリープとSEN0114の測定のタイミング

SEN0114の測定はスリープと測定の手順はスリープとショートスリープを使い分けながら手順1~手順3の手順に沿って行います。

スリープと測定の手順
  • 手順1
    スリープからのウェークアップ

    スリープからウェークアップしたタイミングでSEN0114の電源をONしてnapする。

  • 手順2
    SEN0114の測定結果を取得する

    napからウェークアップしてSEN0114の出力電圧をAD変換値を取得する。

  • 手順3
    無線通信とスリープ

    AD変換値を無線通信する。SEN0114の電源をOFFしてからスリープする。

手順1はトワイライトの9ピンをHIGHにしてSEN0114の電源をONしてnapします。napはSEN0114の電源をONしてから測定値が安定するまでの時間として100msを確保する目的とトワイライトの消費電流を減少させる目的で使用しています。

napが短すぎると消費電流の減少の効果が低くなります。またnapが長いとSEN0114が消費する電流が増えてしまいます。

手順2はSEN0114の出力電圧をAD変換します。

if(Analogue.available()){
 vc2 = Analogue.read(PIN_AD1);
  vc2 = vc2 * 2; //TWE-EH-SのVC2は1/2なので2倍
  sen0114Data = Analogue.read(PIN_AD2); //電圧変換した値
  sen0114Data_raw = Analogue.read_raw(PIN_AD2); //デジット値
}

トワイライトのAnalogueライブラリでread()関数を使用すると電圧変換された値が取得でき、read_raw()関数を使用すると電圧変換前のデジット値が取得できます。

手順3は手順2で取得したTWE-EH-Sの電圧値とSEN0114の計測値を無線通信します。トワイライトの9ピンをLOWにしてSEN0114の電源をOFFしたあとスリープします。

スポンサーリンク

動作確認

袋に包んだトワイライト一式

TWE_VCCがGNDと接続されると初期化処理によってC1間の電圧が約0.08V電圧降下しました。

追加した1000uFのコンデンサ(C1)によって電圧降下を押さえられています。

1000uFを入れていない場合は約0.6V程度電圧降下するためリセットと電源ONが繰り返し行われ動作が安定しませんでした。

余剰電力はBYPを使用していない場合C1の電圧が3.3Vを超えるとハード上の仕組みが働いてC2に電荷が貯まるように構成されています。太陽光パネルの最大出力電圧が5Vですがトワイライトの上限電圧である3.6Vを超えないようにうまく構成されています。

直射日光が当たる環境では電力が高く余剰電力分の電気二重層コンデンサにも数分でチャージできるほどですが、直射日光がTWE-EH-Sの抵抗で電力を消費しきれず電圧が上昇しまうことがあります。

動作確認の方法

朝の8:00に挿し木している植物に水を与えてSEN0114を挿入します。屋外に設置するためSEN0114を除いたモジュール一式をビニール袋に包んで設置します。MWSTAGEフォルダー内のtools内のTWELITEプログラマを起動してログを取りながら通信状況を確認します。

  1. 電気二重層コンデンサの容量は2.2F
  2. 自作アプリを利用し間欠動作の時限を500秒
  3. 植物に水を与えてSEN0114を挿入する
  4. 太陽光パネルを空に向けて設置する
  5. 夕方18:00以降にログを終了して土壌の様子を確認する

動作確認を行った日の天気は朝方は晴で昼頃より曇りで直射日光が当たらない環境となりました。

余剰電力の変化と確認
余剰電力の変化と確認

朝の8:00に屋外の日当たりが良い場所に設置すると30分程度で余剰電力分のコンデンサが2.4V以上チャージされバイパス(BYP)状態になりました。BYPの動作から10分程度でフル充電になりました。直射日光が当たっていたので充電時間が早くなっています。

曇り空になっても昼間で明るさを感じられる場合は電圧が低下することなくフル充電の状態を保つことができています。

広告

動作確認の結果

ターミナルによる動作確認
ターミナルによる動作確認

TWELITEプログラマのターミナル機能を使用して動作確認を行っています。ターミナルログを保存しながら屋外に設置したトワイライト一式の無線データを確認しました。

間欠動作中の土壌の様子
間欠動作中の土壌の様子

青線はTWE-EH-Sが供給しているトワイライトの電源電圧です。橙線はSEN0114から取得したデジット値です。太陽光パネルから十分に電力が得られる環境であったためトワイライトの電源が3.3Vを超えて安定しています。

TWE_VCCとHumid値はAD変換の基準電圧がTWE_VCCを基準にしているため連動した波形になっています。トワイライトの電源が3.3VでSEN0114の出力電圧を調整しましたが、SEN0114の電圧が3.3Vを超えているため830程度と調整値よりも高いデジット値になっています。

18:00に土壌の様子を確認すると土壌が湿っていたのでログのデジット値が示しているin waterの状態と一致する結果になりました。

今回は500秒の間欠動作にしましたが土壌の様子はそう簡単に変わらないため1時間に一度の間欠動作で十分だと感じました。

挿し木している植物の土壌の様子を確認したため土壌の水分量を多く管理していたこともありますが、水分の与え過ぎで枯れてしまう植物を管理する場合に使用できそうです。

スポンサーリンク

ソースコード全体

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

子機のソースコード:

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

#define TIME_UP 0
#define TIME_OFF -1
#define TIME_OUT_MAX 100
#define SLEEP_DUR 500000
#define AD_INIT_VALUE 0xFFFF
#define AD_VCC_CHK 2400 //2.4V

const uint8_t PIN_DO1 = mwx::PIN_DIGITAL::DIO18; //Boot
const uint8_t PIN_DO2 = mwx::PIN_DIGITAL::DIO19; //Byp
const uint8_t PIN_DO3 = mwx::PIN_DIGITAL::DIO9; //sen0114
const uint8_t PIN_AD1 = mwx::PIN_ANALOGUE::A1; //TWE-EH-SのVCC
const uint8_t PIN_AD2 = mwx::PIN_ANALOGUE::A2; //SEN0114

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

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

enum  APP_MODE{
    INTERACTIVE = 0,
    NORMAL
};

/* 変数宣言 */
APP_MODE appmode = APP_MODE::NORMAL;
SYSTEM_TYP twesystem;
MWX_APIRET txreq_stat;
E_STATE eState;
int32_t timTxTimeout = TIME_OFF;
int16_t sen0114Data;
int16_t sen0114Data_raw;
uint16_t twevcc = AD_INIT_VALUE;
uint16_t vc2 = AD_INIT_VALUE;
bool vc2Dosetflg = false;
bool b_senser_started = false;

/* プロトタイプ宣言 */
void mainTimer(void);
void napNow(void);
void sleepNow(void);
MWX_APIRET transmit(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
        pinMode(PIN_DO3, OUTPUT_INIT_LOW );
        Analogue.setup(true, ANALOGUE::KICK_BY_TICKTIMER);
        Analogue.begin(pack_bits(PIN_AD1,PIN_AD2,PIN_ANALOGUE::VCC),5);

        set.reload();
        twesystem.dat.app_di = set.u32appid();
        twesystem.dat.channel = set.u8ch();
        twesystem.dat.logical_id = set.u8devid();
	// the twelite main class
        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();
        //Serial << "start" << crlf << mwx::flush;
    }
}

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

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

  	    do{
                loop_more = false;

                switch(eState){
                    case E_STATE::WORK_JOB:
                        if(Analogue.available()){
                            twevcc = Analogue.read(mwx::PIN_ANALOGUE::VCC);
                            vc2 = Analogue.read(PIN_AD1);
                            vc2 = vc2 * 2; //TWE-EH-SのVC2は1/2なので2倍
                            sen0114Data = Analogue.read(PIN_AD2);
                            sen0114Data_raw = Analogue.read_raw(PIN_AD2);

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

                            Serial 
                                << crlf << format("..%04d/finish sensor capture.", millis() & 8191)
                                << crlf << "  vcc: " << int(vc2)
                                << crlf << "  SEN0114: " << double(sen0114Data)/1000 << " raw: " << sen0114Data_raw << 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 {
                            timTxTimeout = TIME_OFF; 
                            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:
                        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){
        napNow();
    }
}
/* ナップ(ショートスリープ)処理 */
void napNow(){
    uint32_t u32ct = 100;
    the_twelite.sleep(u32ct, false, false, TWENET::SLEEP_WAKETIMER_SECONDARY);
}
/* スリープ処理 */
void sleepNow(){
    uint32_t u32ct;

    digitalWrite(PIN_DO3,LOW);
    u32ct = SLEEP_DUR + random(0,200);
    b_senser_started = false;
    the_twelite.sleep(u32ct);
}
/* 送信パケット準備 */
MWX_APIRET transmit(void) {

    if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
	pkt << tx_addr(0x00)  
   	    << tx_retry(0x2) 
	    << tx_process_immediate();
             
        pack_bytes(pkt.get_payload(),
                    twevcc,
                    vc2,
                   (uint16)sen0114Data_raw,
                   (uint16)sen0114Data
        );
                   
	return pkt.transmit();
    }
    return MWX_APIRET(false, 0);
}
/* callback wakeup */
void wakeup(){

    if (b_senser_started) { //napからwakeup
        eState = E_STATE::WORK_JOB;
        //Serial << "nap-wakeup" << crlf << mwx::flush;
    }
    else{ //sleepからwakeup
        b_senser_started = true;
        digitalWrite(PIN_DO3,HIGH);
        //Serial << "sleep-wakeup" << crlf << mwx::flush;
        napNow();
    }
}

MONOSTICKのソースコード:

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

#define CNT_MAX 1000

/*** Config part */
const uint32_t APP_ID = 0x11223344;
const uint8_t CHANNEL = 13;

int16_t cnt1ms;
uint32_t ts;
bool rcvflg;

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

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

    auto&& brd = the_twelite.board.use<MONOSTICK>();
    brd.set_led_red(LED_TIMER::ON_RX, 200); // RED (on receiving)
    brd.set_led_yellow(LED_TIMER::BLINK, 500); // YELLOW (blinking)
    the_twelite
	<< TWENET::appid(APP_ID)    // set application ID
	<< TWENET::channel(CHANNEL) // set channel
	<< TWENET::rx_when_idle();  // open receive circuit

    auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
    nwksmpl << NWK_SIMPLE::logical_id(0x00);
    the_twelite.begin(); // start twelite!
    Serial << "--- MONOSTICK_sen0114 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;
	}
    }
}

void receive() {
    uint8_t u8DI_BM_remote = 0xff;
    uint16_t vcc;
    uint16_t vc2;
    uint16_t addata_raw;
    uint16_t addata;
    uint8_t lqi;

    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());
    lqi = rx.get_lqi();

    auto&& np = expand_bytes( rx.get_payload().begin(),rx.get_payload().end()
                            ,vcc
			    ,vc2
			    ,addata_raw
			    ,addata
                           );

    Serial << "LQI: " << ((double)7*lqi-1970)/20 << "dBm" 
	   << " Vcc: " << (double)vcc/1000 << "V" 
	   << " Vc2: " << (double)vc2/1000 << "V" 
	   << " Humid: " << int(addata_raw) << " "
           << double(addata)/1000 << "V" 
	   << mwx::crlf << mwx::flush;
}

MWSTAGEの環境でアクト内のソースファイルを置き換えることで使用することができます。

関連リンク

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

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

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

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

PR:エンジニア転職なら100%自社内開発求人に強い【クラウドリンク】

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

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