XBeeでドア開閉センサをつくる 制作編

th_xbee_door_3

XBeeでドアの開閉センサを作ってみたいと思います。ドアの開閉センサも、よくよく考えてみると実は取り付け先とか用途によって要件が変わってくるもので、

  • ドアの種類
    • 基本閉じている状態がデフォルトのもの
      • 例)玄関のドア、冷蔵庫のドア
    • 開きっぱなしの状態も考えられるもの
      • 例)ベランダのドア、窓、室内のドア
  • 用途
    • 状態が変化したときに通知があればOK
    • 定期的に状態を通知してほしい
    • センサの親から問い合わせがあったときに、開閉状態を返してほしい

等々、自分の求めるセンサがどんなものであるのかをちゃんと整理した方がよさそうです。

今回は

  • 基本閉じている状態がデフォルトの玄関ドアへ取り付け
  • 定期的に開閉状態を通知

を満たすものを作りたいと思います。元々は開閉時のみ通知してかつバッテリで長時間駆動、というのを作ろうとしていたのですが、なんかうまく動いてくれなかったので、上記のセンサで一旦手を打つことにしました。

今回は、主に以下を参考にさせていただいています。

それから、以下の本も買い足しました。

この本と『XBeeで作るワイヤレスセンサーネットワーク』 の本があれば、情報としては十分だと思います。

 

さて、まず、ドアの開閉状態をinputできるようにXBeeを配線をします。11番ピン(DIO4)をデジタル入力として利用することにします。ドア開閉センサとして使うリードスイッチに磁石が近づいているとき(=ドアが閉じているとき)リードスイッチはON、磁石が離れたとき(=ドアが開いたとき)リードスイッチはOFFになるので、11番ピンの内部プルアップを有効にして、リードスイッチONのときに11番ピンがGNDに接続されるようにすれば、ドアが開状態のときにはリードスイッチOFFで11番ピンはHIGH、閉状態のときはリードスイッチONで11番ピンはLOWになります。内部プルアップの設定は、後でXBeeの設定の方でやります。といっても、デフォルトで有効になっているので、特に設定することもありませんが。

ついでにスリープ状態の動作確認のために13番ピンにLEDを接続して、ハード的な配線は終了です。

XBeeDoorSensor_coin

電源にはとりあえずコイン電池CR2032を用意してみました。これで動いてくれれば、コンパクトでありがたいのだけれど。

実物はこんな感じです。ほぼ上の図のとおり。

th_xbee_door_1

XBeeは秋月電子のピッチ変換基盤を使ってブレッドボードに接続しています。スペースの関係から、端っこの余分なスペースはカッターで切り落としています。ある程度動作確認ができた時点で、XBeeの通電中に常時点灯するようになっているLEDを消灯させるため、JP1のパターンをカットしています。

リードスイッチは地元の電子部品ショップで10個500円で売られていたものを使っています。でも最初こんなに脆いものだとは知らなくて、線の折り曲げ作業で2個破壊してしまいました。。。秋月でも似たようなものが安く手に入りますし、ちょっと大きくなりますが使いやすく加工されたものにしてもいいと思います。

 

さて、あとはXBeeの設定です。

ウチの玄関ドアを開けてから勝手に閉まるまでの時間を測定してみると、だいたい4〜5秒といったところでした。ということは、4秒毎にスリープ解除してドアの開閉状態をサンプリングしてコーディネータのXBeeに通知してやるようにすれば、ドアの開状態もちゃんとログとして残すことができそうです。

ということで、PM(スリープ・モード)を”Cyclic Sleep [4]”にして、ST(スリープまでの時間)を100ms、SP(スリープする時間)を3900msに設定します。STがちょっと短めかもしれませんが、基本的にはエンドデバイスから一方的にコーディネータに送りつけるだけなので、これぐらいで大丈夫かと思います。ひょっとしたらもう少し短くできるかも…

あと、エンドデバイスが起きている間に1回サンプリングしてくれればよいので、IR(ST期間中のサンプリング周期)はST期間より1msだけ多い101msに設定します。IRがゼロ以外に設定されていれば、スリープ復帰時に必ず1回、入力設定されているピンのサンプリングをしてくれるので、これでOKです。

SN(ON/SLEEPピンをHIGHにするまでにスキップする、SP期間の回数)は、今回は特にON/SLEEPピンを使ってどうこうするつもりはないので、デフォルト値の1のままにしておきます。

SNは、SO(スリープ・オプション)を0x04に設定したときに、SP×SN時間の長時間スリープを実現するためにも使われるようですが、今回はSOは0x00にするので、それも関係ありません。

以上で省電力関係の設定は終了です。あとは、入出力ピンの設定です。

今回は11番ピンを使うので、D4を”Digital Input [3]”に設定します。それから、11番ピンのプルアップを有効にしておく必要がありますが、デフォルトでプルアップは有効になっているので、PRは0x1FFFのままでOKです。

最終的に、エンドデバイスについては、以下のようにX-CTUで設定することになります。Function Setを”ZigBee End Device API”にしてファームウェアに書き込んでから、

  • ID … コーディネータと同じPAN ID
  • DH … 上位アドレス0(コーディネータに送信)
  • DL … 下位アドレス0(コーディネータに送信)
  • BD … 57600(*自分の場合です)
  • AP … 2(*よく分かっていませんが、2で統一しています)
  • SM … Cyclic Sleep [4]
  • ST … 64(100ms)
  • SP … 186(390ms, この10倍が実際のスリープ時間)
  • SN … 1(デフォルト)
  • SO … 0(デフォルト)
  • D4 … Digital Input [3]
  • PR … 1FFF(デフォルト)
  • IR … 65(101ms)

ちなみに、STを短く書き換えてしまうとX-CTUでの書き込みタイミングがシビアになってしまう(=リセットを押してから書き込みをクリックするまでの猶予がなくなる)ので、STは最後に設定することをオススメします。どうにもならなくなったら、X-CTUの”XBee Recovery”で強制的に”ZigBee End Device API”のファームウェアを書き込んで、再度上記の設定をする方が手取り早いと思います。

xbee_recovery

 

さて、エンドデバイスの設定はこれでOKですが、一応エンドデバイスで設定したSPの値と同じかそれ以上の値を、コーディネータとルータのSPに設定しておく必要があります。こうすることで、エンドデバイスがスリープしている間、親ノードがエンドデバイス宛のメッセージを保持しておいてくれるようになります。今回はエンドデバイスからの定期的なメッセージ送信が主な用途なのですが、一応親ノードからの問い合わせにも対応できるようにしておくための処理です。あくまで一応ということなので、応答に最悪(スリープ復帰までの時間である)4秒ぐらいかかってしまいますが、良しとしています。

 

あとは、コーディネータ側のArduino Fioのソース変更が必要です。やれやれ。

以下にソースコードを載せますが、すみません、説明すると長くなってしまうので、説明は省略させてください。必要な方は、自力で読み解いていただければ幸いです。

通信相手として、ルーターXBee1つ、エンドデバイスXBee1つを想定しています。必要最小限のことしか書いていませんし、XBee以外にも自身に直接接続されているセンサのセンシング、I2C通信のためのコードも混じってしまっていますが、XBeeライブラリの使い方の参考にはなるかもしれません。

#include <XBee.h>
#include <Wire.h>
#define SLAVE_ADDRESS 0x21

const uint8_t HIGH_STATE = 1;
const uint8_t LOW_STATE  = 0;
const float SUPPLY_VOLT = 3.3;

uint8_t TEMP_PIN   = A2;
uint8_t HUMID_PIN  = A0;
uint8_t ILL_PIN    = A3;
uint8_t MIC_PIN    = A1;
uint8_t MOTION_PIN = 12;
uint8_t LED_PIN    = 11;

float temp_avg   = 0.0;
float humid_avg  = 0.0;
float ill_avg    = 0.0;
float mic_avg    = 0.0;
uint8_t motion_avg = 0;

uint8_t door_e = 0;

// すべてのセンサは、問い合わせ時点から過去1分間の出力値の平均値を返す。
// ただし、モーションセンサについては、平均値が0.2以上なら「人がいた」と判定して1、
// 0.2未満なら「人がいない」と判定して0を返す
const uint8_t SENSOR_LOG_NUM = 60;
float temp_log[SENSOR_LOG_NUM];
float humid_log[SENSOR_LOG_NUM];
float ill_log[SENSOR_LOG_NUM];
float mic_log[SENSOR_LOG_NUM];
float motion_log[SENSOR_LOG_NUM];

// 玄関ドアの開閉状態については、4秒に1回通知されてくる。
// 過去1分間で1回でも"1"が記録されていたら、
// 「ドアが開いた」として1を返す
const uint8_t SENSOR_LOG_DOOR_NUM = 15;
uint8_t door_e_log[SENSOR_LOG_DOOR_NUM];
uint8_t door_e_log_index = 0;

uint8_t loop_count = 0;
byte sendByte;

//-----------------------------------------------------------------
XBee xbee = XBee();
XBeeResponse response = XBeeResponse();
// create reusable response objects for responses we expect to handle 
ZBRxResponse rx = ZBRxResponse();
ZBRxIoSampleResponse rxio = ZBRxIoSampleResponse();
ModemStatusResponse msr = ModemStatusResponse();
// {温度下位バイト, 温度上位バイト, 湿度下位バイト, 湿度上位バイト, 照度下位バイト, 照度上位バイト, 
//  人感下位バイト, 人感上位バイト, 開閉下位バイト, 開閉上位バイト}
const uint32_t XBEE_ADDRESS_LIVING = 0x40CA2F26;
uint8_t receiveXBeeData_living[] ={0,0,0,0,0,0,0,0,0,0};
const uint32_t XBEE_ADDRESS_ENTRANCE = 0x40ADD42D;
//-----------------------------------------------------------------

float getTemperature(){
  int LM35DZ_Value = analogRead(TEMP_PIN);
  return ((SUPPLY_VOLT * LM35DZ_Value) / 1024) * 100;
}

void updateTemperatureAvg(){
  float sum = 0.0;
  for(uint8_t i=0;i<SENSOR_LOG_NUM;i++){
    sum += temp_log[i];
  }
  temp_avg = sum/SENSOR_LOG_NUM;
}

float getHumidity(float temp){
  int HIH4030_Value = analogRead(HUMID_PIN);
  float voltage  = HIH4030_Value/1024.0 * SUPPLY_VOLT;
  float sensorRH = 161.0 * voltage / SUPPLY_VOLT - 35;
  float trueRH   = sensorRH / (1.0546 - 0.00216 * temp);
  return trueRH;
}

void updateHumidityAvg(){
  float sum = 0.0;
  for(uint8_t i=0;i<SENSOR_LOG_NUM;i++){
    sum += humid_log[i];
  }
  humid_avg = sum/SENSOR_LOG_NUM;
}

float getIlluminance(){
  int AMS302_Value = analogRead(ILL_PIN);
  float lx = AMS302_Value*(3300.0/1024.0)/1000.0/(0.26/100);
  return lx;
}

void updateIlluminanceAvg(){
  float sum = 0.0;
  for(uint8_t i=0;i<SENSOR_LOG_NUM;i++){
    sum += ill_log[i];
  }
  ill_avg = sum/SENSOR_LOG_NUM;
}

float getMic(){
  float ADMP401_Value = analogRead(MIC_PIN);
  return 3.3*ADMP401_Value/1024.0;
}

void updateMicAvg(){
  float sum = 0.0;
  for(uint8_t i=0;i<SENSOR_LOG_NUM;i++){
    sum += mic_log[i];
  }
  mic_avg = sum/SENSOR_LOG_NUM;
}

void updateMotionAvg(){
  float sum = 0.0;
  motion_avg = 0;
  for(uint8_t i=0;i<SENSOR_LOG_NUM;i++){
    sum += motion_log[i];
  }
  float avg = sum/SENSOR_LOG_NUM;
  if(avg >= 0.2){
    motion_avg = 1;
  }
}

void updateDoorE(){
  door_e = 0;
  for(uint8_t i=0;i<SENSOR_LOG_DOOR_NUM;i++){
    if(door_e_log[i] == 1){
      door_e = 1;
      break;
    }
  }
}

void receiveData(int byteCount){
  uint8_t i2c_command = -1;
  while(Wire.available()){
    i2c_command = Wire.read();
    //Serial.print("i2c data received: ");
    //Serial.println(i2c_command);
    switch(i2c_command){
      case 0xF0: updateTemperatureAvg();
                 sendByte = (uint8_t)(int(temp_avg*100)); break;
      case 0xF1: sendByte = (uint8_t)(int(temp_avg*100) >> 8); break;
      case 0xF2: updateHumidityAvg();
                 sendByte = (uint8_t)(int(humid_avg*100)); break;
      case 0xF3: sendByte = (uint8_t)(int(humid_avg*100) >> 8); break;
      case 0xF4: updateIlluminanceAvg();
                 sendByte = (uint8_t)(int(ill_avg)); break;
      case 0xF5: sendByte = (uint8_t)(int(ill_avg) >> 8); break;
      case 0xF6: updateMicAvg();
                 sendByte = (uint8_t)(int(mic_avg*100)); break;
      case 0xF7: sendByte = (uint8_t)(int(mic_avg*100) >> 8); break;
      case 0xF8: updateMotionAvg();
                 sendByte = motion_avg; break;
      case 0xF9: sendByte = 0; break;
      // ------------------------------------------------------------------
      case 0xE0: sendByte = receiveXBeeData_living[0]; break;
      case 0xE1: sendByte = receiveXBeeData_living[1]; break;
      case 0xE2: sendByte = receiveXBeeData_living[2]; break;
      case 0xE3: sendByte = receiveXBeeData_living[3]; break;
      case 0xE4: sendByte = receiveXBeeData_living[4]; break;
      case 0xE5: sendByte = receiveXBeeData_living[5]; break;
      case 0xE6: sendByte = receiveXBeeData_living[6]; break;
      case 0xE7: sendByte = receiveXBeeData_living[7]; break;
      case 0xE8: sendByte = receiveXBeeData_living[8]; break;
      case 0xE9: sendByte = receiveXBeeData_living[9]; break;
      // ------------------------------------------------------------------
      case 0xD0: updateDoorE();
                 sendByte = door_e; break;
      case 0xD1: sendByte = 0; break;
    }
  }
}

void sendData(){
  Wire.write(sendByte);
}

void setup() {
  // put your setup code here, to run once:
  Serial.begin(57600);  // シリアル通信速度
  xbee.begin(Serial);

  pinMode(TEMP_PIN, INPUT);
  pinMode(HUMID_PIN, INPUT);
  pinMode(ILL_PIN, INPUT);
  pinMode(MIC_PIN, INPUT);
  pinMode(MOTION_PIN, INPUT);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);

  Wire.begin(SLAVE_ADDRESS);
  Wire.onReceive(receiveData);
  Wire.onRequest(sendData);

  for(uint8_t i=0;i<SENSOR_LOG_NUM;i++){
    temp_log[i]   = 0.0;
    humid_log[i]  = 0.0;
    ill_log[i]    = 0.0;
    mic_log[i]    = 0.0;
    motion_log[i] = 0.0;
  }

  //Serial.println("Ready.");
}

void loop() {

  xbee.readPacket();

  if (xbee.getResponse().isAvailable()) {
    // got something
    digitalWrite(LED_PIN, HIGH);
    delay(100);
    digitalWrite(LED_PIN, LOW);
    delay(100);

    if (xbee.getResponse().getApiId() == ZB_RX_RESPONSE) { // APIモードフレームタイプ 0x90: RX受信
      // センサステーションからの受信は、LEDを計3回点滅させる
      digitalWrite(LED_PIN, HIGH);
      delay(100);
      digitalWrite(LED_PIN, LOW);
      delay(100);
      digitalWrite(LED_PIN, HIGH);
      delay(100);
      digitalWrite(LED_PIN, LOW);  
      // got a zb rx packet

      // now fill our zb rx class
      xbee.getResponse().getZBRxResponse(rx);

      // どこのXBeeからデータから送られてきたデータか確認
      XBeeAddress64 address64 = rx.getRemoteAddress64();
      uint32_t address64_lsb = address64.getLsb();// 上位バイトはXBeeで共通なので、下位バイトでアドレスを判断する
      if(address64_lsb == XBEE_ADDRESS_LIVING){
        // リビングのXBeeから送られてきたデータの場合
        uint8_t payload_length = rx.getDataLength();
        //uint8_t offset = rx.getDataOffset();
        uint8_t* payload = rx.getData();
        for(int i=0;i<payload_length-1;i++){
          receiveXBeeData_living[i] = payload[i];
        }
      }

      if (rx.getOption() == ZB_PACKET_ACKNOWLEDGED) { // 受信オプション 0x01: パケットには確認応答が返された
          // the sender got an ACK
          //flashLed(statusLed, 10, 10);
      } else {
          // we got it (obviously) but sender didn't get an ACK
          //flashLed(errorLed, 2, 20);
      }
      // set dataLed PWM to value of the first byte in the data
      //analogWrite(dataLed, rx.getData(0));      
    } else if (xbee.getResponse().getApiId() == ZB_IO_SAMPLE_RESPONSE) { // APIモードフレームタイプ 0x92: 入出力データサンプル受信通知
      // 玄関のXBeeからの受信は、LEDを計2回点滅させる
      digitalWrite(LED_PIN, HIGH);
      delay(100);
      digitalWrite(LED_PIN, LOW);

      xbee.getResponse().getZBRxIoSampleResponse(rxio);
      // どこのXBeeからデータから送られてきたデータか確認
      XBeeAddress64 address64 = rxio.getRemoteAddress64();
      uint32_t address64_lsb = address64.getLsb();// 上位バイトはXBeeで共通なので、下位バイトでアドレスを判断する
      if(address64_lsb == XBEE_ADDRESS_ENTRANCE){
        // 玄関のXBeeから送られてきたデータの場合
        if(rxio.isDigitalEnabled(4)){ // ドアの開閉情報はDIO4を利用しているため
          //receiveXBeeData_entrance[0] = rxio.isDigitalOn(4);
          if(door_e_log_index == SENSOR_LOG_DOOR_NUM){
            door_e_log_index = 0;
          }
          door_e_log[door_e_log_index] = (uint8_t)rxio.isDigitalOn(4);
          door_e_log_index++;
        }
      }
    } else if (xbee.getResponse().getApiId() == MODEM_STATUS_RESPONSE) { // APIモードフレームタイプ 0x8a: モデムステータス
      xbee.getResponse().getModemStatusResponse(msr);
      // the local XBee sends this response on certain events, like association/dissociation

      if (msr.getStatus() == ASSOCIATED) {
        // yay this is great.  flash led
        //flashLed(statusLed, 10, 10);
      } else if (msr.getStatus() == DISASSOCIATED) {
        // this is awful.. flash led to show our discontent
        //flashLed(errorLed, 10, 10);
      } else {
        // another status
        //flashLed(statusLed, 5, 10);
      }
    } else {
    	// not something we were expecting
      //flashLed(errorLed, 1, 25);    
    }
  } else if (xbee.getResponse().isError()) {
    //nss.print("Error reading packet.  Error code: ");  
    //nss.println(xbee.getResponse().getErrorCode());
  }

  if(loop_count == SENSOR_LOG_NUM){
    loop_count = 0;
  }
  temp_log[loop_count]   = getTemperature();
  humid_log[loop_count]  = getHumidity(temp_log[loop_count]);
  ill_log[loop_count]    = getIlluminance();
  mic_log[loop_count]    = getMic();
  motion_log[loop_count] = (float)digitalRead(MOTION_PIN);

  loop_count++; 

  delay(1000);
}

これで動作確認してみたところ、とりあえず開閉センサ側XBeeからコーディネータXBeeへの定期送信はうまく動いてくれているようです。コーディネータXBeeからの要求応答は未確認です。

th_xbee_door_2

開閉センサ側XBeeからデータを受信すると、ラピロのArduino Fioシールドに繋がったLEDビームサーベルが点滅するようにしています。LEDビームサーベルについては、「ビームライフル(赤外線リモコン銃)は持たせたから、次はビームサーベルだ!」ということで、エイヤで作りました。思ったより綺麗に光ってくれなかったのが残念ですが、とりあえず良しとしています。

ドアに取り付けてみると、こんな感じです。とりあえず両面テープで貼り付けて、ドアが金属製だったので、リードスイッチの対抗には、Arduinoシールドをラピロの腕に取り付けるときに余ったネオジム磁石を貼り付けています。

th_xbee_door_3 th_xbee_door_4

うん、なかなか良い感じじゃないでしょうか。

あとは、コイン電池(CR2032)でどれだけ長期間動いてくれるか、です。ものすごくざっくり計算してみたところ、一応計算上は22.5〜39.0日ぐらいは持ってくれる…はずなのですが、そうは問屋が卸さないようで。

次回、検証編に続きます。