ESP-WROOM-02とMQTT + openHABでホームオートメーションに挑戦 (5) ドップラーセンサーモジュールの追加

秋月電子通商で購入したドップラーセンサーモジュールを前回製作したArduCAMの回路に追加しました。

■ドップラーセンサーモジュール(24GHz)DIP化セット NJR4265 J1(24GHz)

OpenHABにはメールを送るアクションがあります。訪問者があった時、ドップラーセンサーで感知して画像をメールする一連の動作を考えました。が、しかし。結論から言いますと、上手く動作しませんでした。今回は失敗ですが、備忘のために作業した記録を残します。

Arducam回路図 02

Arducam回路図 02

NJR4265J1は説明によると、24GHz帯マイクロ波ドップラーによる移動体検知を行う人感センサ(最大10m)で、単一電源3.3V~5.0V動作です。シリアルインターフェースはCMOSレベル、10KΩ抵抗でプルアップされてます。

ArduCAM + NJR4265J1写真

ArduCAM + NJR4265J1

OTAで書き込みができるようになりましたので、一度スケッチを書き込んだ後には書き込みのためにシリアルポートを使わなくてすみます。そちらにNJR4265J1を繋ぎました。

ボーレート:9600bps データビット:8bit パリティ:奇数(odd) ストップbit:1 フロー制御:なし で通信します。また、後からコマンドを送りセンサーの感度調節を行うことができます。

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>

#include <Wire.h>
#include <ArduCAM.h>
#include <SPI.h>
#include "memorysaver.h"

#include <ESP8266mDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>

#include <PubSubClient.h>

// Enabe debug tracing to Serial port.
#define DEBUGGING

// Here we define a maximum framelength to 64 bytes. Default is 256.
#define MAX_FRAME_LENGTH 64

// Define how many callback functions you have. Default is 1.
#define CALLBACK_FUNCTIONS 1


const int CS = 16;


long lastMsg = 0;
char msg[50];
int value = 0;
String inputString = "";         // a string to hold incoming data  //serial
boolean stringComplete = false;  // whether the string is complete  //serial

int wifiType = 0; // 0:Station  1:AP
const char* ssid = "xxxxxxx"; // Put your SSID here
const char* password = "xxxxxxx"; // Put your PASSWORD here
const char* mqtt_server = "xxx.xxx.xxx.xxx"; // mqtt broker IP Address

IPAddress ip(192, 168, 11, 1);     //ESP-WROOM-02 IP Address 
IPAddress gateway(192, 168, 11, xxx); // Gateway IP Address
IPAddress subnet(255, 255, 255, 0);  // Subnet Mask

WiFiClient espClient;
PubSubClient mqtt_client(espClient);

ESP8266WebServer server(80);

ArduCAM myCAM(OV2640, CS);

void start_capture() {
  myCAM.clear_fifo_flag();
  myCAM.start_capture();
}

void camCapture(ArduCAM myCAM) {
  WiFiClient client = server.client();

  size_t length = myCAM.read_fifo_length();
  if (length >= 393216) {
    //    Serial.println("Over size.");
    return;
  } else if (length == 0 ) {
    //    Serial.println("Size is 0.");
    return;
  }

  myCAM.CS_LOW();
  myCAM.set_fifo_burst();
  SPI.transfer(0xFF);

  if (!client.connected()) return;
  String response = "HTTP/1.1 200 OK\r\n";
  response += "Content-Type: image/jpeg\r\n";
  response += "Content-Length: " + String(length) + "\r\n\r\n";
  server.sendContent(response);

  static const size_t bufferSize = 4096 ; // 1024
  static uint8_t buffer[bufferSize] = {0xFF};

  while (length) {
    size_t will_copy = (length < bufferSize) ? length : bufferSize;
    SPI.transferBytes(&buffer[0], &buffer[0], will_copy);
    if (!client.connected()) break;
    client.write(&buffer[0], will_copy);
    length -= will_copy;
  }

  myCAM.CS_HIGH();
}

void serverCapture() {
  start_capture();
  //  Serial.println("CAM Capturing");

  int total_time = 0;

  total_time = millis();
  while (!myCAM.get_bit(ARDUCHIP_TRIG, CAP_DONE_MASK))
  {
    delay(1);
  }


  total_time = millis() - total_time;
  //  Serial.print("capture total_time used (in miliseconds):");
  //  Serial.println(total_time, DEC);

  total_time = 0;

  //  Serial.println("CAM Capture Done!");
  total_time = millis();
  camCapture(myCAM);
  total_time = millis() - total_time;
  //  Serial.print("send total_time used (in miliseconds):");
  //  Serial.println(total_time, DEC);
  //  Serial.println("CAM send Done!");
}

void serverStream() {
  WiFiClient client = server.client();

  String response = "HTTP/1.1 200 OK\r\n";
  response += "Content-Type: multipart/x-mixed-replace; boundary=frame\r\n\r\n";
  server.sendContent(response);

  while (1) {
    start_capture();
    delay(1);
    while (!myCAM.get_bit(ARDUCHIP_TRIG, CAP_DONE_MASK))
    {

    }
    size_t length = myCAM.read_fifo_length();
    if (length >= 393216) {
      //      Serial.println("Over size.");
      continue;
    } else if (length == 0 ) {
      //      Serial.println("Size is 0.");
      continue;
    }

    myCAM.CS_LOW();
    myCAM.set_fifo_burst();
    SPI.transfer(0xFF);

    if (!client.connected()) break;
    response = "--frame\r\n";
    response += "Content-Type: image/jpeg\r\n\r\n";
    server.sendContent(response);

    static const size_t bufferSize = 4096  ; //  1024
    static uint8_t buffer[bufferSize] = {0xFF};

    while (length) {
      size_t will_copy = (length < bufferSize) ? length : bufferSize;
      SPI.transferBytes(&buffer[0], &buffer[0], will_copy);
      if (!client.connected()) break;
      client.write(&buffer[0], will_copy);
      length -= will_copy;
    }
    myCAM.CS_HIGH();

    if (!client.connected()) break;
  }
}

void handleNotFound() {
  String message = "Server is running!\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  server.send(200, "text/plain", message);

  if (server.hasArg("ql")) {
    int ql = server.arg("ql").toInt();
    myCAM.OV2640_set_JPEG_size(ql);
    //    Serial.println("QL change to: " + server.arg("ql"));
  }
}

void mqtt_callback(char* topic, byte* payload, unsigned int length) {
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.print("\r\n");
}

void mqtt_reconnect() {
  while (!mqtt_client.connected()) {
    if (mqtt_client.connect("ESP8266Client_02")) {
      mqtt_client.publish("outTopic", "hello world");
      mqtt_client.subscribe("/home/gate/motion/com");
    } else {
      delay(5000);
    }
  }
}
void serialEvent() {
  while (Serial.available()) {
    char inChar = (char)Serial.read();
    if (inChar != '\n' && inChar != '\r') {
      inputString += inChar;
    }
    if (inChar == '\n') {
      stringComplete = true;
    }
  }
}

void setup() {
  uint8_t vid, pid;
  uint8_t temp;
#if defined(__SAM3X8E__)
  Wire1.begin();
#else
  Wire.begin();
#endif
  //  Serial.begin(115200);
  //  Serial.println("ArduCAM Start!");

  // set the CS as an output:
  pinMode(CS, OUTPUT);

  // initialize SPI:
  SPI.begin();
  SPI.setFrequency(4000000); //4MHz

  //Check if the ArduCAM SPI bus is OK
  myCAM.write_reg(ARDUCHIP_TEST1, 0x55);
  temp = myCAM.read_reg(ARDUCHIP_TEST1);
  if (temp != 0x55) {
    //    Serial.println("SPI1 interface Error!");
    while (1);
  }

  //Check if the camera module type is OV2640
  myCAM.wrSensorReg8_8(0xff, 0x01);
  myCAM.rdSensorReg8_8(OV2640_CHIPID_HIGH, &vid);
  myCAM.rdSensorReg8_8(OV2640_CHIPID_LOW, &pid);
  if ((vid != 0x26) || (pid != 0x42)) {
    //    Serial.println("Can't find OV2640 module!");
    while (1);
  } else {
    //    Serial.println("OV2640 detected.");
  }

  //Change to JPEG capture mode and initialize the OV2640 module
  myCAM.set_format(JPEG);
  myCAM.InitCAM();
  myCAM.OV2640_set_JPEG_size(OV2640_640x480);
  myCAM.clear_fifo_flag();
  myCAM.write_reg(ARDUCHIP_FRAMES, 0x00);

  if (wifiType == 0) {
    // Connect to WiFi network
    //    Serial.println();
    //    Serial.println();
    //    Serial.print("Connecting to ");
    //    Serial.println(ssid);

    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
    //set static ip part
    WiFi.config(ip, gateway, subnet);

    while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      //      Serial.print(".");

    }
    //    Serial.println("WiFi connected");
    //    Serial.println("");
    //    Serial.println(WiFi.localIP());
  } else if (wifiType == 1) {
    //    Serial.println();
    //    Serial.println();
    //    Serial.print("Share AP: ");
    //    Serial.println(ssid);

    WiFi.mode(WIFI_AP);
    WiFi.softAP(ssid, password);
    //    Serial.println("");
    //    Serial.println(WiFi.softAPIP());
  }

  // Start the server
  server.on("/capture", HTTP_GET, serverCapture);
  server.on("/stream", HTTP_GET, serverStream);
  server.onNotFound(handleNotFound);
  server.begin();
  //  Serial.println("Server started");

  ArduinoOTA.begin();
  //    Serial.println("OTA Ready");
  //    Serial.print("IP address: ");
  //    Serial.println(WiFi.localIP());


  Serial.begin(9600, SERIAL_8O1);
  // reserve 200 bytes for the inputString:  //serial
  inputString.reserve(200);  //serial
  // NJR4264J default threshold setting
  delay(5000);               // START UP
  Serial.print("@SM550");   // SENSOR SENSITIVITY (Leaving 1-999 Integer)
  Serial.print("\r\n");
  Serial.print("@SP550");   // SENSOR SENSITIVITY (coming 1-999 Integer)
  Serial.print("\r\n");
  // setup_wifi();
  mqtt_client.setServer(mqtt_server, 1883);
  mqtt_client.setCallback(mqtt_callback);



}

void loop() {
  ArduinoOTA.handle();
  server.handleClient();

  if (!mqtt_client.connected()) {
    mqtt_reconnect();
  }
  mqtt_client.loop();

  serialEvent(); //call the function
  if (stringComplete) {
    if (inputString == "@C") {
      inputString = "coming";
    }
    else if (inputString == "@L") {
      inputString = "leaving";
    }
    else if (inputString == "@N") {
      inputString = "no_movement";
    }

    inputString.toCharArray(msg, 50);
    mqtt_client.publish("/home/gate/motion/state", msg);
    inputString = "";
    stringComplete = false;
  }

  long now = millis();
  if (now - lastMsg > 30000) {
    lastMsg = now;
    ++value;
    snprintf (msg, 75, "hello world from motion_sensor #%ld", value);
    mqtt_client.publish("outTopic", msg);   // keep_alive to  mosquitte
  }

}

前回のスケッチにNJR4265J1の制御を付け加え、MQTTブローカーを経由してセンサーの値をやりとりするようにしました。シリアルインターフェースを通して送られていたメッセージをすべてコメントアウトです。

Topic /home/gate/motion/state へ NJR4265J1は "coming" "leaving" "no_movement" をパブリッシュします
Topic /hoem/gate/motion/com   へメッセージを送るとNJR4265J1を制御できます。

NJR4265J1は移動体が接近すると ”@C“ また移動体が離反すると “@L” 動きが無いときは”@N“を送ってきます。 (正確にはそれぞれの文字の後ろにCR:キャリッジリターン と LF:ラインフィードが付加されます) 今回は上記欄のように”coming” “leaving” “no_movement”と文字列変換しました。以前使用したMQTTlensで動作の様子を見ることができます。

MQTTlens 01

MQTTlens 01

MQTTlens で MQTTブローカーへ接続後 トピック /home/gate/motion/state をsubclibeするとセンサーから送られてきた情報が表示されるようになります。
(こちらにMQTTlensの使い方 )

同様に トピック /home/gate/motion/com へ ”@SP?” とコマンドを送ります。これは現在設定されている近接感度(閾値)を応答する命令です。図MQTTlens 02に示されている例では700が応答されています。今回のスケッチでは感度の初期設定値を近接と離反、双方とも550にしましたが、1~999の範囲で設定することができます。NJR4265J1の取り扱い説明書は秋月電子のweb siteからダウンロードすることができます。
http://akizukidenshi.com/download/ds/jrc/NJR4265J1_Rev00.pdf

MQTTlens 02

MQTTlens 02

続いてOpenHABの設定です。まず、メールを扱えるようにするためにopenhab-addon-action-mail をインストールします。(Debian Wheezyでの手順になります)

# apt-get update
# apt-get upgrade
# apt-get install openhab-addon-action-mail

次に /etc/openhab/openhab.cfg を編集します。今回はGmailのメールサーバを使って送る設定にしました。

######################## Mail Action configuration ####################################
#
# The SMTP server hostname, e.g. "smtp.gmail.com"
#Gmail の送信メールサーバ
mail:hostname=smtp.gmail.com

# the SMTP port to use (optional, defaults to 25 (resp. 587 for TLS))
#Submissionポート指定
mail:port=587

# the username and password if the SMTP server requires authentication
#Gmailのアドレス(ユーザー名)
mail:username=xxxxxx@gmail.com  
#パスワード
mail:password=xxxxxx

# The email address to use for sending mails
#Gmailのアドレス(または自分が使っている別のメールアドレス)
mail:from=xxxxx@gmail.com

# set to "true", if TLS should be used for the connection
# (optional, defaults to false)
# TLSを使う
mail:tls=true

# set to "true", if POP before SMTP (another authentication mechanism)
# should be enabled. Username and Password are taken from the above
# configuration (optional, default to false)
#mail:popbeforesmtp=

itemsファイルに追記です /etc/openhab/configurations/my_home.items

String motion_gate "Motion Sensor [%s]"  {mqtt="<[broker:/home/gate/motion/state:state:default]"}

最後に新規rulesファイルを作成します。センサーが"coming"を通知してきたときにメールを送るようにしました。見よう見まねでconcurrentロックを加えましたが、間違っているかもしれません。

/etc/openhab/configurations/rules/sendmail.rules

import java.util.concurrent.locks.ReentrantLock

var java.util.concurrent.locks.ReentrantLock lock  = new java.util.concurrent.locks.ReentrantLock()

rule "Send Mail"

when
  Item motion_gate changed to coming
then
  lock.lock()
  try{
     sendMail("xxxx@hogehoge.com","Motion Sensor","There is motion","http://192.168.11.1/capture")
} finally{
     lock.unlock()
}
end

送付先等は sendMail("宛先メールアドレス","題名","本文","画像URL")のように指定します。
また、ESP-WROOM-02に設定したIPアドレスを便宜上192.168.11.1とします。

OpenHABをリスタートして動作確認をしますが、最初に述べた通り次の2点で動作不良でした。

(1) センサーが反応してから画像キャプチャまでのディレイが長すぎて目的の絵が撮れない。メール送信までの一連の動作にかなり時間がかかる。
(2) センサーの感度が悪い、誤作動が頻発する。

(1)についてはある程度予測はしていました。画像添付メールは送られて来ますが、残念ながら毎回肝心な人物が写ってないです。写ってもらうためには5~10秒以上カメラの前に居てもらわなくてはなりません。ArduCAM + esp8266の性能限界なのでしょうね。また、mosquittoとOpenHABが動いているDebianサーバのスペックも十分でない可能性があります。今回はメール送信はあきらめてリアルタイム映像表示に専念します。将来別のWebCAMを購入することがあったら、また試してみます。(早くRaspberry Pi Zeroが安く手に入るようにならないかな。 )

電波が干渉してしまった?

電波が干渉してしまった?

(2)の原因が当初わからなかったのですが、どうやらESP-Wroom-02とNJR4265J1間の電波干渉が原因みたいでした。考えてみれば双方ともGHzの高周波を使用しています。昔なら私みたいな素人が扱える周波数帯ではなかったハズです。(技術の進歩はすごいですね) 感度設定に依ると思いますが、最低でも30cm程度離して設置しないと誤作動してしまうようです。

その後、先日作成した気温・気圧・湿度測定のボードをMQTT仕様にして画像と共に表示させるようにしました。(こちらも一波乱ありましたが...)

PCからOpenHAB 画面の表示にはChrome のアドオン Mini OpenHAB Controller が便利です。

OpenHABからの表示

OpenHABからの表示

ESP-WROOM-02とMQTT + openHABでホームオートメーションに挑戦 (4) ArduCam(web camera)とOTA (無線でスケッチ書き込み)

OpenHABでは画像も表示されることがわかりましたので、何か良いものはないかと物色していたところ、EbayでArducamを見つけました。
ArduCAM01
Arducam Mini module Camera Shield w/ 2 MP OV2640 for Arduino UNO Mega2560 board

200万画素のCMOSイメージセンサー OV2640搭載でインターフェースはSPIとI2CでAruinoにつなげることができるみたいです。3.3V動作可能なのでESP-WROOM-02とも相性が良さそうですね。製造元のサイトにESP8266との配線図が載っていますので無線WEBカメラが簡単に出来そうです。(実際は私の不徳で長い道のりでした)

ArduCAMのサイト
http://www.arducam.com/arducam-supports-esp8266-arduino-board-wifi-websocket-camera-demo/

Arducam回路図01

Arducam回路図01

ArduCAMライブラリーのインストールを行います。
https://github.com/ArduCAM/Arduino
こちらのサイトから DownloadZIPでファイルをダウンロードして、解凍したできたArduCAMディレクトリをそのままArduinoのlibraryフォルダにコピーします。Arduino IDE から

ファイル > スケッチの例 > ArcuCAM > ESP8266 > ArduCAM_mini_Ov2640_Capture

をロードします。

const char* ssid = "SSID"; // Put your SSID here
const char* password = "Password"; // Put your PASSWORD here

自分のWifi環境に合わせ上記二つのを書き換えてスケッチを走らせてみます。ちなみに今回私は Arduino IDE ver 1.6.7 , ボードマネージャー esp8266 by ESP8266 Community Ver 2.1.0 で作業を行いました。

実行してみると、シリアルモニタの表示は

ArduCAM Start!
Can't find OV2640 module!

残念ながらArduCAM MINI(ArduCAM-M-2MP)には何種類かリビジョン違いがあるみたいで、私の物はこのスケッチでは動かないようです。スケッチの一部を変更します。

if ((vid != 0x26) || (pid != 0x41)){
Serial.println("Can't find OV2640 module!");

ここの行の 0x41を0x42に書き換えて、ようやくOV2640イメージセンサーを認識するようにしました。

動作の確認方法です。直接ブラウザでアドレス指定しても良いのですが、ダウンロードして展開したファイル

libraryes > ArduCAM > examples > ESP8266 > ArduCAM_Mini_OV2640_Capture > html > index.html

こちらにある index.html(またはvideo.html)をブラウザで立ち上げます。起動時シリアルモニタに表示されるDHCPで割り振られたESP-WROOM-02のIPアドレスをCamera IP Address欄にインプットした後、下のいずれかの解像度のボタンをクリックするとキャプチャした画面が表示されます。

画像が乱れてしまった

画像が乱れてしまった

…という訳だったのですが、残念!ちゃんと表示されません。

https://github.com/ArduCAM/Arduino/issues/21 こちらに2つの解決法が載っていました。

1. ESP8266 libraryのコードを手直しする方法 ボードマネージャでesp8266をインストールした場合は下記の場所のコードを一部書き換えます。(書き換え内容はURL先記述文)

ユーザー > (User NAME) > AppData(隠しフォルダ) > Local > Arduino15 > packages > esp8266 > hardware > esp8266 > (version NO.) > libraryes > ESP8266WiFi > SRC以下

2.スケッチ中のバッファサイズを書き換える方法 (4096を1024へ)

static const size_t bufferSize = 1024; //4096 2ケ所

今回は 2. 案を採用しました

画面表示速度ははっきり言って遅いです。640×480の解像度では大体2~3秒で1画面描画でしょうか。0.5~0.3fpsぐらいだと思います。(後付けの注釈:書いている私が言うのはおかしいですが、実用性だけを考えると別のwebcamを選択した方が良いと思います。)

ArduCAMとESP-WROOM-02の転送速度がネックになっているようです。ただ、320×240の解像度では割と早く表示されます。また、1.案でスケッチをコンパルすると速度が速くなるかもしれません。(後付けの注釈:気持ち早くなりました)

ようやく表示されました

ようやく表示されました

さて私個人的には、過去さんざん失敗してきたOTA(Over The Aire : WiFi経由のスケッチ書き込み)に挑戦です。Arduino IDE ver 1.6.7より正式対応になったようです。こちらtakehikoshimajimaさんのサイトに詳しく書かれています。

上記サイトの文中に記述されてますが、下記プログラムがインストールされていることが必要です。

■ Arduino IDE ver 1.6.7と Board : esp8266 by ESP8266 Community ver 2.0.0 (or later)
■ Python 2.7

さらに、マルチキャストDNSを使ってホストの名前解決という事なので、この他にWindowsマシン + Arduino IDEの構成ではBonjourも必要だと思います。iTunesがインストールされていれば同時に入っているかもしれません。(※ Windows10ではBonjourは必要ないようです)

Bonjour
サンプルスケッチに少し手を加え、スタティック IP AdressとOTAを設定します。


#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>

#include <Wire.h>
#include <ArduCAM.h>
#include <SPI.h>
#include "memorysaver.h"

#include <ESP8266mDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>

// Enabe debug tracing to Serial port.
#define DEBUGGING

// Here we define a maximum framelength to 64 bytes. Default is 256.
#define MAX_FRAME_LENGTH 64

// Define how many callback functions you have. Default is 1.
#define CALLBACK_FUNCTIONS 1

const int CS = 16;

int wifiType = 0; // 0:Station  1:AP
const char* ssid = "SSID"; // Put your SSID here
const char* password = "PASSWORD"; // Put your PASSWORD here

IPAddress ip(192, 168, 11, 1); //ESP-WROOM-02 IP Address
IPAddress gateway(192, 168, 11, xxx); //Gateway IP Address
IPAddress subnet(255, 255, 255, 0); //SubNet MASK

ESP8266WebServer server(80);

ArduCAM myCAM(OV2640, CS);

void start_capture() {
  myCAM.clear_fifo_flag();
  myCAM.start_capture();
}

void camCapture(ArduCAM myCAM) {
  WiFiClient client = server.client();

  size_t length = myCAM.read_fifo_length();
  if (length >= 393216) {
    Serial.println("Over size.");
    return;
  } else if (length == 0 ) {
    Serial.println("Size is 0.");
    return;
  }

  myCAM.CS_LOW();
  myCAM.set_fifo_burst();
  SPI.transfer(0xFF);

  if (!client.connected()) return;
  String response = "HTTP/1.1 200 OK\r\n";
  response += "Content-Type: image/jpeg\r\n";
  response += "Content-Length: " + String(length) + "\r\n\r\n";
  server.sendContent(response);

  static const size_t bufferSize = 1024 ; // 4096
  static uint8_t buffer[bufferSize] = {0xFF};

  while (length) {
    size_t will_copy = (length < bufferSize) ? length : bufferSize;
    SPI.transferBytes(&buffer[0], &buffer[0], will_copy);
    if (!client.connected()) break;
    client.write(&buffer[0], will_copy);
    length -= will_copy;
  }

  myCAM.CS_HIGH();
}

void serverCapture() {
  start_capture();
  Serial.println("CAM Capturing");

  int total_time = 0;

  total_time = millis();
  while (!myCAM.get_bit(ARDUCHIP_TRIG, CAP_DONE_MASK))
  {
    delay(1);
  }


  total_time = millis() - total_time;
  Serial.print("capture total_time used (in miliseconds):");
  Serial.println(total_time, DEC);

  total_time = 0;

  Serial.println("CAM Capture Done!");
  total_time = millis();
  camCapture(myCAM);
  total_time = millis() - total_time;
  Serial.print("send total_time used (in miliseconds):");
  Serial.println(total_time, DEC);
  Serial.println("CAM send Done!");
}

void serverStream() {
  WiFiClient client = server.client();

  String response = "HTTP/1.1 200 OK\r\n";
  response += "Content-Type: multipart/x-mixed-replace; boundary=frame\r\n\r\n";
  server.sendContent(response);

  while (1) {
    start_capture();
    delay(1);
    while (!myCAM.get_bit(ARDUCHIP_TRIG, CAP_DONE_MASK))
    {

    }
    size_t length = myCAM.read_fifo_length();
    if (length >= 393216) {
      Serial.println("Over size.");
      continue;
    } else if (length == 0 ) {
      Serial.println("Size is 0.");
      continue;
    }

    myCAM.CS_LOW();
    myCAM.set_fifo_burst();
    SPI.transfer(0xFF);

    if (!client.connected()) break;
    response = "--frame\r\n";
    response += "Content-Type: image/jpeg\r\n\r\n";
    server.sendContent(response);

    static const size_t bufferSize = 1024  ; //  4096
    static uint8_t buffer[bufferSize] = {0xFF};

    while (length) {
      size_t will_copy = (length < bufferSize) ? length : bufferSize;
      SPI.transferBytes(&buffer[0], &buffer[0], will_copy);
      if (!client.connected()) break;
      client.write(&buffer[0], will_copy);
      length -= will_copy;
    }
    myCAM.CS_HIGH();

    if (!client.connected()) break;
  }
}

void handleNotFound() {
  String message = "Server is running!\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  server.send(200, "text/plain", message);

  if (server.hasArg("ql")) {
    int ql = server.arg("ql").toInt();
    myCAM.OV2640_set_JPEG_size(ql);
    Serial.println("QL change to: " + server.arg("ql"));
  }
}

void setup() {
  uint8_t vid, pid;
  uint8_t temp;
#if defined(__SAM3X8E__)
  Wire1.begin();
#else
  Wire.begin();
#endif
  Serial.begin(115200);
  Serial.println("ArduCAM Start!");

  // set the CS as an output:
  pinMode(CS, OUTPUT);

  // initialize SPI:
  SPI.begin();
  SPI.setFrequency(4000000); //4MHz

  //Check if the ArduCAM SPI bus is OK
  myCAM.write_reg(ARDUCHIP_TEST1, 0x55);
  temp = myCAM.read_reg(ARDUCHIP_TEST1);
  if (temp != 0x55) {
    Serial.println("SPI1 interface Error!");
    while (1);
  }

  //Check if the camera module type is OV2640
  myCAM.wrSensorReg8_8(0xff, 0x01);
  myCAM.rdSensorReg8_8(OV2640_CHIPID_HIGH, &vid);
  myCAM.rdSensorReg8_8(OV2640_CHIPID_LOW, &pid);
  if ((vid != 0x26) || (pid != 0x42)) {
    Serial.println("Can't find OV2640 module!");
    while (1);
  } else {
    Serial.println("OV2640 detected.");
  }

  //Change to JPEG capture mode and initialize the OV2640 module
  myCAM.set_format(JPEG);
  myCAM.InitCAM();
  myCAM.OV2640_set_JPEG_size(OV2640_640x480);
  myCAM.clear_fifo_flag();
  myCAM.write_reg(ARDUCHIP_FRAMES, 0x00);

  if (wifiType == 0) {
    // Connect to WiFi network
    Serial.println();
    Serial.println();
    Serial.print("Connecting to ");
    Serial.println(ssid);

    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
    //set static ip part
    WiFi.config(ip, gateway, subnet);

    while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      Serial.print(".");

    }
    Serial.println("WiFi connected");
    Serial.println("");
    Serial.println(WiFi.localIP());
  } else if (wifiType == 1) {
    Serial.println();
    Serial.println();
    Serial.print("Share AP: ");
    Serial.println(ssid);

    WiFi.mode(WIFI_AP);
    WiFi.softAP(ssid, password);
    Serial.println("");
    Serial.println(WiFi.softAPIP());
  }

  // Start the server
  server.on("/capture", HTTP_GET, serverCapture);
  server.on("/stream", HTTP_GET, serverStream);
  server.onNotFound(handleNotFound);
  server.begin();
  Serial.println("Server started");

  ArduinoOTA.begin();
  //  Serial.println("OTA Ready");
  //  Serial.print("IP address: ");
  //  Serial.println(WiFi.localIP());
}

void loop() {
  ArduinoOTA.handle();
  server.handleClient();
}


最初は通常通りシリアルインターフェースからスケッチを書き込みます。再起動してESP-WROOM-02がネットワークに繋がった後に再度 Arduino IDEを立ち上げると、新たにネットワークインターフェースが表示されます。

新しい無線経由のポートが表示されます

新しいWiFi経由のポートが表示されます


インターフェースを切り替えてスケッチをアップロードしたのですが残念ながら、

[ERROR]: No response from device 

エラーメッセージが表示されてうまくいきません。なかなか一筋縄ではいかないようです。

かなり悩みましたが結局、失敗の原因はPCにインストールされているセキュリティソフトのファイヤーウォールでした。外部から始まった内向きの通信がブロックされているのが問題だったようです。(盲点でした)

すべてクリアして初めてOTAが成功したときは感動モノでした。スピードも速いですし、すごく便利ですね。デプロイ(設置)した後でもスケッチを簡単に書き換えられそうです。(注:WiFi経由のポートを選択した時はシリアルモニタは使えないようです。)

画像はブラウザから直接下記のアドレスでもアクセスできます。(便宜上 ESP-WROOM-02のIPアドレスを192.168.11.1とします)

http://192.168.11.1/capture :静止画
http://192.168.11.1/stream :動画

これをOpenHABから表示させるようにします。本来OpenHABではmjpeg動画も表示できるのですが、今回のESP-WROOM-02 + ArduCAM では表示可能なものの長時間の運用がどうもうまくいきませんでした。仕方なく静止画を数秒ごとに再表示させることにします。

# cat /etc/openhab/configurations/sitemaps/my_home.sitemap

sitemap my_home label="Main Menu"
{
Frame label="MQTT" {
Switch item=lamp1 label="Gatepost Lamp"

Frame label="" {
Image url="http://192.168.11.1/capture" refresh=1000
}
}

1000ミリ秒(= 1 S)ごとに画面をリフレッシュする指定なのですが、残念ながら私のOpenHab 1.8.1では呼び出した時のままで、再描写されませんでした。これにも相当悩みましたが、こちらに書かれているとおりコードを変更して解決しました。(ディレクトリは Debian wheezy の場合です)

(※ 3/20追記 この方法で画像はリフレッシュされるようになりましたが、他のアイテムがリフレッシュされないようです。一長一短です。)

(※ 3/27 追記 Debian wheezy にて apt-get でインストールされるOpenHABのバージョンが 1.8.2になったようです。下記の作業は必要ないと思います。)

https://community.openhab.org/t/please-help-test-fix-for-image-and-chart-refresh-in-classic-web-ui/8106

# /etc/init.d/openhab stop
# mkdir ~/temp
# cd /usr/share/openhab/server/plugins
# mv org.openhab.ui.webapp_1.8.1.jar ~/temp/
# cd /usr/share/openhab/addons/
# wget https://dl.dropboxusercontent.com/u/4286376/classicui-full-image-chart-refresh/org.openhab.ui.webapp-1.8.2-SNAPSHOT.jar
# /etc/init.d/openhab start
OpenHABから表示されました

OpenHABから表示されました

ESP-WROOM-02とMQTT + openHABでホームオートメーションに挑戦 (3)

前回までで門柱灯(gatepost lamp)のリモート操作ができるようになりましたので、いよいよ自動化の設定に入ります。

とりあえず次のような動作を考えました。

  1. 昼行灯(ひるあんどん)は良くないので明るいときは無条件にOFF
  2. 日没後、暗くなったらON
  3. PM9:00にOFF

EEスイッチを置き換えたので他の項目は実現できなくても1の項目だけは何としてもクリアしたい所です。

前回までに光センサーの値はESP-WROOM-02側からMQTT Brokerの /home/gatepost/sensor/stateへパブリッシュされるようになっていますので、今回はOpenHAB側のconfigを一部追加して表示させるようにします。本来光センサーの値をリアルタイムに表示してもあまり意味をなさないのですが、今回は自動化の設定値を求めるために一時的に行います。(シリアルモニタで見るより簡単・手間いらずです)

■itemsの編集

$ sudo vi /etc/openhab/configurations/items/my_home.items
Group All

Switch lamp1 "gatepost Lamp" (all){mqtt=">[broker:/home/gatepost/lamp/com:command:on:ON],>[broker:/home/gatepost/lamp/com:command:off:OFF],<[broker:/home/gatepost/lamp/state:state:default]"}

/* 以下を追記します */
Number light_sensor "光センサー (Light Sensor) [%d ]"  {mqtt="<[broker:/home/gatepost/sensor/state:state:default]"}

■sitemapの編集

$ sudo vi /etc/openhab/configurations/sitemaps/my_home.sitemap
sitemap my_home label="Main Menu"
{
   Frame label="MQTT" {
   Switch item=lamp1 label="Gatepost Lamp"
   /* 以下を追記します */
   Text item=light_sensor
  }  
}

■OpehnHABの再起動

$ sudo  /etc/init.d/openhab restart

openhab03-1

光センサーの値がスイッチの下の行に表示されるようになりました。さて、さらにこれを進めて一日の推移をグラフにして表示してみます。

 

 

 

 

先日紹介した https://openhardwarecoza.wordpress.com/2015/03/30/openhab-mqtt-arduino-and-esp8266-part-5-1-graphing-sensor-data/ こちらのサイトを参考に設定を行います。

■rrd4j add-onをインストール

$ sudo apt-get install openhab-addon-persistence-rrd4j

上記はdemoサイトが設定されていれば追加されるみたいです。そのときは不要です。

■persistenceファイルの編集

$ sudo vi /etc/openhab/configurations/persistence/rrd4j.persist
Strategies {
        // for rrd charts, we need a cron strategy
        everyMinute : "0 * * * * ?"
}

Items {

light_sensor : strategy = everyMinute

}

■sitemapの編集

$ sudo vi /etc/openhab/configurations/sitemaps/my_home.sitemap
sitemap my_home label="Main Menu"
{
   Frame label="MQTT" {
   Switch item=lamp1 label="Gatepost Lamp"
   Text item=light_sensor
   
   /* 以下を追記します */
   Frame label="フォトトランジスタの出力をAD変換した値" {
        Chart label="light_sensor" item=light_sensor period=d refresh=30000
  }
 }
}

■OpehnHABの再起動

$ sudo  /etc/init.d/openhab restart

openhab03-2

再起動後しばらくするとグラフが描かれ始めます。period=d としたので24時間のグラフになりました。このようにOpenHABではとても簡単にグラフを描くことができます。

こちら に書かれていますがデータをグループで指定して複数のグラフを書くこともできるようです。ただ残念ながら現在のところ複数のY軸の設定は出来ないようなので異なるカテゴリーの値を一つのグラフに表示させることは難しいようです。また、設定が悪いのかバグなのかは分かりませんが、私のiOSアプリではなぜかグラフが表示されません。(サファリなどのブラウザからは表示されます)

観察の結果、だいたいセンサーの値が25を下回ったら電灯をONにすれば良さそうです。電灯をOFFにする条件は、センサーの値の変動を考慮して50を超えた値で無条件にOFFとしましょう。先ほども申しましたが光センサーの値のグラフはあまり意味をなさないので、将来的には気温のグラフを表示させようと思っています。

余談ですが、当初は光センサーのフォトトランジスタの向きが良くなかったようで、暗いときの感度が低くNGでした。センサーを外付けにして調整したころ改善しました。

(失敗) 間接的に外の光を計測していたので感度が不足してしまった

(失敗) 間接的に外の光を計測していたので感度が不足してしまった

さて、ようやく準備ができました。いよいよルールを設定する番です。OpenHABではJAVA言語を発展させたXtendという言語でルールを記述するようです。JAVAすら初めてな私にとっては、ハードルが極めて高そうですね。でもそこは、いつもどおり「見よう見まね」で切り抜けていきましょう。

https://github.com/openhab/openhab/wiki/Rules やhttps://github.com/openhab/openhab/wiki/Samples-Rules を参考にして、だいたい次の事がわかりました。

● rules ファイルはテキストで記述され、

[インポート:Imports]   [変数宣言:Variable Declarations]   [ルール:Rules]

3セクション構成となっている

rules file 例 (https://github.com/openhab/openhab/wiki/Samples-Rules#how-to-turn-on-light-when-motion-detected-and-is-dark 記載)

    var Number counter = 0
    var Number lastCheck = 0

    rule "corLightOn"
    when   
            Item corMotion changed from OFF to ON or
            Item corFrontDoor changed from CLOSED to OPEN
    then   
            counter = counter + 1
            if(corBright.state < 40) {
                    sendCommand(corLightCeil, ON)
            }
    end

    rule "corLightOff"
    when   
            Time cron "0 * * * * ?"
    then   
            if(lastCheck == counter) {
                    counter = 0
                    lastCheck = -1;
                    sendCommand(corLightCeil, OFF)
                    sendCommand(corMotion, OFF)
            } else {
                    lastCheck = counter
            }
    end  

上記の通りルールは一つのファイルに複数記入しても良いみたいですね。正直言って [インポート:Imports] は全く理解不能ですが、この例のように簡単なルールでは必要ないみたいです。恐らくArduino IDEで言えばライブラリみたいなものでしょう。まぁ、動作しなかった時に考えましょう。何か読み込ませばきっと動くでしょう。たぶん。

● ルールが起動するトリガは次の3つ

  1. Itemベーストリガ —– コマンドが発行されたりステータスが変更になったときに起動
  2. Timeベーストリガ —– 時間指定による起動
  3. Systemベーストリガ—- システムの開始時などに起動

Itemベーストリガ 
    Item  received command []
    Item  received update []
    Item  changed [from ] [to ]

Timeベーストリガ
    Time is midnight
    Time is noon
    Time cron ""

TimeベーストリガではLinuxのcronライクな表現で指定できるのですね。ただ こちら を見るとlinux の cronにくらべ機能が拡張されているようです。左から順に

秒 分 時 日 月 曜日 年(オプション)

秒や年が指定できるようです。persistenceファイルの編集の時にも出てきました。ワイルドカードの*はともかくとして、?マークは見慣れないですね。どうやら日と曜日はどちらかを指定して指定しない方に?をつけるようです(たぶん)。という訳で16時台から20時台の15分毎に起動させたければ “0 0/15 16-20 * * ?” と指定すれば良いですね。

以上をふまえて、当初計画した自動化ルールを作成します。

  1. 光センサー(Item:light_sensor)の値を調べ、50より大きいときは門柱灯(Item:lamp1)へOFFのコマンドを送る。[1分おきに実行]
  2. 光センサーの値が25より小さい時はONのコマンドを送る。[16時から20時45分まで15分おきに実行]
  3. 門柱灯へOFFのコマンドを送る。[21時0分0秒に実行]

サンプルを見ると日没時間等を計算式で求める事も可能なようですね。まったくもってアメージングです。でも、あまり欲をかいてしまうと失敗してしまいますので、身の丈にあった設定ファイルにしておきます。

■gatepost.rulesの作成

$ sudo vi /etc/openhab/configurations/rules/gatepost.rules
rule "LightOFF_in_daylight"
    when
                Time cron "0 * * * * ?"
    then
                if (light_sensor.state > 50){
                   sendCommand(lamp1, OFF)
                }

rule "LightON_after_sunset"
    when
                Time cron "0 0/15 16-20 * * ?"
    then
                if (lamp1.state == OFF && light_sensor.state < 25){
                   sendCommand(lamp1, ON)
                }
                

rule "LightOff_at_21"
    when
                Time cron "0 0 21 * * ?"
    then
                    sendCommand(lamp1, OFF)
    end

■OpehnHABの再起動

$ sudo  /etc/init.d/openhab restart

11月22日 東京での日の入り時間は16時31分でした。 16時45分に門柱灯が自動的に点灯しましたので、どうやらうまく行ったようです。ログを見てみます。

$ sudo cat /var/log/openhab/events.log

....
2015-11-22 16:28:55 - light_sensor state updated to 54
2015-11-22 16:29:00 - lamp1 received command OFF
2015-11-22 16:29:00 - lamp1 state updated to OFF
2015-11-22 16:29:00 - light_sensor state updated to 53
....
2015-11-22 16:29:55 - light_sensor state updated to 50
2015-11-22 16:30:00 - light_sensor state updated to 51
2015-11-22 16:30:05 - light_sensor state updated to 46
....
2015-11-22 16:44:56 - light_sensor state updated to 24
2015-11-22 16:45:00 - lamp1 received command ON
2015-11-22 16:45:00 - lamp1 state updated to ON
2015-11-22 16:45:01 - light_sensor state updated to 24
2015-11-22 16:45:06 - light_sensor state updated to 24
2015-11-22 16:45:11 - light_sensor state updated to 25
....

光センサーの値が5秒間隔で記録されています。必要以上に間隔が短すぎるかもしれませんね、後で調整することにします。

光センサーを外付けにしました。

光センサーを外付けにしました

ESP-WROOM-02とMQTT + openHABでホームオートメーションに挑戦 (2)

とある日 工作をしている私に家人から質問がありました

家人:「一体その品物に、いくら掛かっているのですか?」私:「2,000~3,000円ぐらいかな」家人:「また、また、尾戯れを」私:「本当ですってば」

しかし嘘ではありません。一番高価な部品はesp-wroom-02モージュールで、これが約1000円。AC100VーDC5V電源とケースが各400円程。SSRキットが250円。+その他部品ですから大体3,000円以内で収まっていると思いますよ。

(しかしながら、原因不明で電源が壊れてしまい廃棄処分になった1号機の部品代や将来使うかもしれないと思い購入してしまったドップラーセンサーモジュール、スケッチ書き込みのための超小型USBシリアル変換モジュール、予備の部品、etcを合計すると、それなりの金額になっているのは間違いない事実ではあります。)

回路図

回路図

回路図は基本的に前回と同じですが次の変更を加えました。AC100V-DC5V電源にe-bayから仕入れた Ultra-compact power module HLK-PM01(出力5v 0.6A)を使い、電灯線制御にソリッドリレー。 さらに将来の自動化を目的にフォトトランジスタによる照度センサー。そしてローカルで電灯を制御するためにSW3を新設しました。

【デプロイした2号機】USBケーブルの先端にUSBシリアル変換モジュール。

【デプロイした2号機】USBケーブルの先端にUSBシリアル変換モジュール。スケッチを書き込むために仮にAC100Vへ配線して通電しています。 スケッチの書き込み時は100Vに通電せずUSB側から給電するようにします。(100vに通電して書き込みを行うと最悪の場合PCを破損する可能性があります。)

技術もないのに省スペースを追求しすぎて1号機は失敗してしまいました。教訓を活かして2号機は秋月電子の 【ユニバーサル基板 Bタイプ(95x72mm)】とケースにホームセンターで仕入れた【PVKボックス(プールボックス) 未来工業】に集積度を上げず余裕を持って作ることにしました。

既存の電灯配線に制御回路を繋げなくてはいけませんが、これが結構大変です。制御回路を配置する場所が問題となります。通常、電灯は図の様に配線されているので設置候補として図中A,B,Cの場所があります。Bは一番良い場所なのですが、大抵見えない屋根裏とかに隠蔽されているので設置や保守が大変です。

Cからは何とか回路を引き出すことが出来ますが2芯ケーブルの線間電圧が100VではないのでESP-WROOM-02のための電源が別途必要になります。

残るはAの電灯直下ですが、こちらは雨風にさられることになりますので、そちらの対策が必要になります。

配線01

たまたま私の家の門柱灯はEE-Switch【暗くなると自動的にONになる】が露出配線で繋がっていて、この場所にEE-Switchと入れ替える形で制御回路を設置することができました。

(ちなみにこの配線工事は自宅であっても法律上、電気工事士の有資格者が行う必要があるので注意が必要です。)

家庭などのAC配線では線間電圧(100V)という概念のほかに対地電圧という概念が重要になります。アースされているのが接地側で感電の危険性が高いのが非接地側となります。

電気器具もなるべく感電しないようになっていて、白熱電球の人が触れやすいネジの部分の電極は接地側になるよう配線されています。同じ理由で差し込むときガイドになるコンセントの左側が右側より大きく、接地側に接続されています。

また、配線の色は原則として接地側が白又は緑で、非接地側には他の色を使う決まりになっています。(場合により例外もあります)
EE_switch
テスターや未来工業の「検電くん」を使い調査した結果、自宅のEE-スイッチに来ている3芯ケーブルも白が接地側、黒が非接地側、赤が電灯に繋がるように配線されていました。

ESP-WROOM-02のArduinoスケッチは前回のものを少し手直しいたしました。変更点は以下の通りです。

●アナログインターフェースPIN 【TOUT】に繋がっている照度センサのフォトトランジスタの値を analogread(A0) 命令で0Vから1Vを0~1024の値に変換して読み込みます。

手持ちの部品、回路では全く光が入らない状態でも0にならず、大体20ぐらいの値が読み込まれました。この値はMQTT Broker /home/gatepost/sensor/stateへパブリッシュしてOpenhabに伝えます。

【失敗してしまった1号機】

失敗してしまった1号機

失敗した1号機はこの値が不安定だったのでいろいろ弄っている内に、なぜか電源が壊れてしまったのでした。

●DHCPからスタティックIPアドレスに変更しました。DHCPで受け取ったアドレスにはリース期間が設定されているので、その期間が過ぎると一旦接続が切れてしまいます。

●手元で電灯を操作するために 【IO4】にSWを新設しました。当初、割り込みを使ってスケッチを記述しようとしたのですが、読み込んでいるライブラリと相性が悪いのかうまく動作しませんでした。

今回はOPENHARDWARE.CO.ZAさんのコードを使わせていただきました。ただ、このスケッチではMQTTサーバに繋がっていないと動作しません。改善の余地があります。

●ESP-WROOM-02がバックグラウンドでTCP等の処理をする時間を確保するためにloop()の最後に delay(10) を挿入しました。

●SSRをスイッチするためにトランジスタを1段入れましたのでON/OFFの極性が逆になりました。またloop()中 mills()がオーバーフローしたとき(起動してから5ヶ月後)挙動がわからないのでabs()で絶対値の差で比較しました。

/*
 Basic ESP8266 MQTT example

 This sketch demonstrates the capabilities of the pubsub library in combination
 with the ESP8266 board/library.

  2015/10/12 改変
  - subscribes to the topic "/home/gatepost/lamp/com", printing out any messages
   - If the characters of the topic "/home/gatepost/lamp/com" are "ON", switch ON the ESP Led, "OFF" switch off
 
  2015/11/08 改変
  - phototransistor (照度センサー)  analogRead(A0) 
  - Static IP
  - a physical button on IO4 (turns the device on/off)
    --> https://openhardwarecoza.wordpress.com/2015/04/05/openhab-mqtt-arduino-and-esp8266-part-5-2-combined-switch-and-sensor-module-based-on-arduino/
*/ 


#include <ESP8266WiFi.h>
#include <PubSubClient.h>

// Update these with values suitable for your network.

const char* ssid = "your_SSID";
const char* password = "your_Password";
const char* mqtt_server = "192.168.xx.xx";  // mqttサーバのアドレスを設定します。
IPAddress ip(192,168,xx,xx);
IPAddress gateway(192,168,xx,xx);
IPAddress subnet(255,255,255,0);
 
WiFiClient espClient;
PubSubClient client(espClient);
long lastMsg = 0;
char msg[50];
int value = 0;
int buttonPin = 4;
boolean buttonState = LOW; //stroage for current button state
boolean lastState = LOW;//storage for last button state
boolean OutputPinState = LOW;
int analog_val;
char illumination[5];

void setup() {
  pinMode(5, OUTPUT);     // Initialize the 5 pin as an output
  Serial.begin(115200);
  setup_wifi();
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
}

void setup_wifi() {

  delay(10);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);
  //set static ip part
  WiFi.config(ip,gateway,subnet);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }


  // check whether ON or OFF
  if( payload[0] == 'O' && payload[1] == 'N' ) {
    digitalWrite( 5, HIGH);
    OutputPinState = HIGH;
    client.publish("/home/gatepost/lamp/state", "ON"); 
  }
  else if( payload[0] == 'O' && payload[1] == 'F' && payload[2] == 'F' ) {
    digitalWrite( 5, LOW );
    OutputPinState = LOW;
    client.publish("/home/gatepost/lamp/state", "OFF"); 
  }
  else {
    Serial.print( "Unknown command : " );
    // Serial.println( (char*)payload );
  }
  Serial.println();
}

void reconnect() {
  // Loop until we're reconnected 
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    
    
    if (client.connect("ESP8266Client")) {
      Serial.println("connected");
      // Once connected, publish an announcement...
      client.publish("outTopic", "hello world");
      // ... and resubscribe
      client.subscribe("/home/gatepost/lamp/com");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}
void loop() {

  if (!client.connected()) {
    reconnect();
  }
  client.loop();

 unsigned long now = millis();
  if (abs(now - lastMsg) > 5000) {
    lastMsg = now;
    analog_val = analogRead(A0);                 // read photoTr
    itoa(analog_val,illumination,10);
    client.publish("/home/gatepost/sensor/state",illumination);    
    ++value;
    snprintf (msg, sizeof(msg), "hello world from Gatepost. ILLUMINATION=%u #%ld  ",analog_val, value);
    Serial.print("Publish message: ");
    Serial.println(msg);
    client.publish("outTopic", msg);

  }
  buttonState = digitalRead(buttonPin);
  if (buttonState == LOW && lastState == HIGH){   //if button has just been pressed
    Serial.println("pressed");
    delay(300);                                   //prevent from chattering
    
    //toggle the state of the LED
    if (OutputPinState == HIGH){
      digitalWrite(5, LOW);
      OutputPinState = LOW;
      client.publish("/home/gatepost/lamp/state", "OFF");

    } else {
      digitalWrite(5, HIGH);
      OutputPinState = HIGH;
       client.publish("/home/gatepost/lamp/state", "ON");
    }
  }
  lastState = buttonState;
  delay(10);
}

デプロイ後、スマートフォンから電灯のON/OFFが出来るようになりました。めでたしめでたしです。でもこれで終了ではありません。まだまだ続きます。

リモート操作する門柱灯です

リモート操作する門柱灯です

ESP-WROOM-02とMQTT + openHABでホームオートメーションに挑戦 (1)

前回はMQTTプロトコルとESP-WROOM-02を使ってLEDをリモート操作しましたが、LEDだけでは物足りないので自宅の門柱灯を操作する目標を立てました。この電灯は少し不便なところにスイッチがあって普段は残念ながらほとんど使われていません。遠隔操作が出来れば便利に使えそうです。完成までの道のりは長そうですが一つずつクリアしていきたいと思います。

前回はCHROMEアプリのMQTTlensからLEDを操作しましたが、電灯を操作するのに毎回トピック等を入力するのでは、あまりにも操作性が悪いです。そこでオープンソフトウエアのopenHABというホームオートメーションソフトを導入しました。

openHABホーム http://www.openhab.org/

openHabはこちらに書かれていたので知ったのですが、インターネットで検索しても日本語の解説があまり無いようです。多機能ですが設定も難しそうです。いつものことですが今回も手探りの状態です。

openHABの特徴です。

  • webやiOS,Androidから操作するユーザーインターフェースがある。
  • 色々なデバイスやシステム(スイッチのON,OFFのみならずセンサーの値を表示する機能,その他諸々)を統合して管理することができる。
  • 制御に関するルール(時間、近接、明暗、温度、センサーで読んだ値に関連付け)をプログラムすることができる。
  • Java 1.7.の走るWindows, MacOS X or Linuxマシンで動かすことができる。などなど

MQTTのプロトコルにも対応していますので、前回インストールしたMQTTブローカーと連携することができます。リモート操作だけではなく、センサーやタイマーと連動するインテリジェントな動作も構築可能ですね。

上記のサイトにインストール方法等の記述がありましたので、それに沿ってインストールを行いました。(以下の記述はほぼ上記サイトの転記になってしまいました。)

openHab (version 2012/10/10現在 1.7.1) 今回は例によって自宅サーバ Debian Weezyにインストールしました。 Raspberry Pi 2にもインストールされて使われている実績があるようなので将来的にはそちらに移行するかも知れません。

java ver 1.6 以上がインストールされていることが必要だということなので調べます。

$ java -version

条件を満たしていない時

$ sudo apt-get install openjdk-7-jre

$ sudo update-alternatives --config java

openHAB本体のインストールです

$ wget -qO - 'https://bintray.com/user/downloadSubjectPublicKey?username=openhab' | sudo apt-key add -
$ echo "deb http://dl.bintray.com/openhab/apt-repo stable main" | sudo tee /etc/apt/sources.list.d/openhab.list

$ sudo apt-get update
$ sudo apt-get install openhab-runtime openhab-addon-binding-mqtt 

openHABが自動起動するようにします。私の環境の場合次のコマンドで設定しました。

$ sudo update-rc.d openhab defaults

openHAB本体のインストールが完了しましたので デモコンフィグレーションをダウンロードします。

$ wget https://bintray.com/artifact/download/openhab/bin/distribution-1.7.1-demo.zip

$ unzip distribution-1.7.1-demo.zip

$ sudo cp configurations /usr/share/openhab/
$ sudo cp addons /etc/openhab/

openHABを起動してデモを見てみます。

$ sudo /etc/init.d/openhab start

ブラウザでアクセスします。IPアドレスはインストールしたサーバのIPアドレスに読み替えてください。

http://192.168.xx.xx:8080/openhab.app?sitemap=demo

デモサイト

デモサイト

このデモを見ると照明や窓の開閉スイッチ操作だけではなく、気温や窓などのセンサーの値も表示することができるのがわかります。スイッチ操作も簡単ですね。また、ブラウザだけではなくAndroidやiOSのユーザーインターフェースも用意されています。下記はiOSのアプリです。

https://itunes.apple.com/jp/app/openhab/id492054521?mt=8

iOSへアプリをインストール後デモを見るための設定です

setting より
openHAB URL
 Local    http:サーバIPアドレス:8080
 Remode   同上

MISC
 Demo mode のチェックをはずす

 Select sitemap  Demo Houseを選択

またopenHABは非常に多くの機能をアドオンとして使うことができるようです。詳しくは下記で参照できます。

https://github.com/openhab/openhab/wiki/Linux—OS-X

さて今度はopenHabを自分の環境にカスタマイズしていきましょう。今回は動作確認ですので最低限の設定を行います。ハードは前回使用したブレッドボードに組んだ物を使います。まずサーバ側の設定です。

■opnehab.cfgの設定

$ sudo cp /etc/openhab/configurations/openhab_default.cfg /etc/openhab/configurations/openhab.cfg
$ sudo vi /etc/openhab/configurations/openhab.cfg

今回はMQTTブローカーと同じサーバにインストールしたので、下記の設定をopenhab.cfgの最下行に書き加えます。

mqtt:broker.url=tcp://127.0.0.1:1883
mqtt:broker.clientId=openHab

■sitemapの作成

$ sudo vi /etc/openhab/configurations/sitemaps/my_home.sitemap
sitemap my_home label="Main Menu"
{
   Frame label="MQTT" {
   Switch item=lamp1 label="Gatepost Lamp"
  }  
}

■itemsの作成

$ sudo vi /etc/openhab/configurations/items/my_home.items
Group All

Switch lamp1 "gatepost Lamp" (all){mqtt=">[broker:/home/gatepost/lamp/com:command:on:ON],>[broker:/home/gatepost/lamp/com:command:off:OFF],<[broker:/home/gatepost/lamp/state:state:default]"}

次にesp-wroom-02クライアント側です。 前回使用したarduinoスケッチを一部手直しします。

/* 
 Basic ESP8266 MQTT example

 This sketch demonstrates the capabilities of the pubsub library in combination
 with the ESP8266 board/library.

*/ 2015/10/12 改変
  - subscribes to the topic "/home/gatepost/lamp/com", printing out any messages
   - If the characters of the topic "/home/gatepost/lamp/com" are "ON", switch ON the ESP Led, "OFF" switch off
/*  

*/
#include <ESP8266WiFi.h>
#include <PubSubClient.h>

// Update these with values suitable for your network.

const char* ssid = "your_SSID";
const char* password = "your_Password";
const char* mqtt_server = "192.168.xx.xx";  // mqttサーバのアドレスを設定します。

WiFiClient espClient;
PubSubClient client(espClient);
long lastMsg = 0;
char msg[50];
int value = 0;
//unsigned char LampStatus = LOW;

void setup() {
  pinMode(5, OUTPUT);     // Initialize the 5 pin as an output
  Serial.begin(115200);
  setup_wifi();
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
}

void setup_wifi() {

  delay(10);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }


  // check whether ON or OFF
  if( payload[0] == 'O' && payload[1] == 'N' ) {
    digitalWrite( 5, LOW );
    client.publish("/home/gatepost/lamp/state", "ON"); 
  }
  else if( payload[0] == 'O' && payload[1] == 'F' && payload[2] == 'F' ) {
    digitalWrite( 5, HIGH );
    client.publish("/home/gatepost/lamp/state", "OFF"); 
  }
  else {
    Serial.print( "Unknown command : " );
    // Serial.println( (char*)payload );
  }
  Serial.println();
}

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect("ESP8266Client")) {
      Serial.println("connected");
      // Once connected, publish an announcement...
      client.publish("outTopic", "hello world");
      // ... and resubscribe
      client.subscribe("/home/gatepost/lamp/com");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}
void loop() {

  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  long now = millis();
  if (now - lastMsg > 2000) {
    lastMsg = now;
    ++value;
    snprintf (msg, 75, "hello world #%ld", value);
    Serial.print("Publish message: ");
    Serial.println(msg);
    client.publish("outTopic", msg);
  }
}

動作のフローです。openHABがwebや携帯端末から命令をうけるとトピック名 "/home/gatepost/lamp/com"へ"ON"又は"OFF"をパブリッシュします。esp-wroom-02側は同じトピックをサブクライブしていて電灯(LED)の点灯消灯の処理を行います。またesp-wroom-02側からはトピック名"/home/gatepost/lamp/state"にて現在の状態をパブリッシュしてopenHABに知らせることができます。

■動作確認です

  • ブラウザより
    http://192.168.xx.xx:8080/openhab.app?sitemap=my_home
  • iOSアプリより
    setting -MISC
    Select sitemap Main Menuを選択

スイッチをクリック(タップ)してesp-wroom-02側のLEDが点灯又は消灯すれば成功です。

作成したサイト

作成したサイト

ESP-WROOM-02とArdinoIDEでMQTTを使った遠隔Lチカの実験

前回はESP-WROOM-02 (ESP8266)を使って気温・湿度・気圧の記録を行いました。我ながらまずまずの出来だと思ったので家族に少し自慢をしたのですが「そんな物作って何になんの?」と鼻で笑われてしまいました。(ショックです) 以前何かで読みましたが、かの発明王エジソンが蓄音機を発明し、喜び勇んでプレス発表した時の新聞記者も同じ反応だったそうです。

大エジソンと私とでは比べようもないのですが、同じ悲哀を味わえてホンの少しですが人間的に成長できたような気がします。めげずに前進したいと思います。

さて、今回はIoTに適したと言われているMQTTというプロトコルの実験を行いました。ArduinoにはMQTTのライブラリがあって、非常に簡単に実現できることがわかりました。今回はこれを使って遠隔操作でLEDの点灯・消灯を行います。

MQTTについてはこちらShiguredo Inc.様のサイトに詳しい解説があります。私は始めて勉強したのですが、メールマガジンの発行購読に似たシステムの様に感じました。

メッセージの送り手(パブリッシャー),MQTTサーバ(ブローカー),メッセージの購読者(サブクライバー)三者がいて、パブリッシャーはメッセージにトピック(識別子)を付けてMQTTサーバに送ると、MQTTサーバはそのトピックを購読しているサブクライバーにメッセージを配信する仕組みのようです。パブリッシャーは同時にサブクライバーにもなる事ができます。

何から手をつければ良いかわからなかったのですが、こちらのサイトhttp://www.element14.com/community/community/design-challenges/forget-me-not/blog/2014/09/17/fmnxx-mqtt–the-language-of-iotにとても詳しく記述されておりました。 (thanks! vish)
このサイトの記述に沿って実験を行います。

全体の構成は次の図の様になります。
構成図
Localに作るMQTTサーバは当初 Raspberry Pi と思ったのですが、今回は毎度お世話になっている自宅サーバ Debian Linux (wheezy) にインストールすることにしました。brokerソフトはmosquittoになります。次のコマンドでインストールします。

sudo apt-get install mosquitto mosquitto-clients python-mosquitto  

私の場合残念ながら上記だけではMQTT v3.1.1 Broker に対応するバージョンがインストールされず、Arduino スケッチがうまく動いてくれなかったので、ここに書いてある通り追加の作業を行いました。

wget http://repo.mosquitto.org/debian/mosquitto-repo.gpg.key
sudo apt-key add mosquitto-repo.gpg.key

cd /etc/apt/sources.list.d/

sudo wget http://repo.mosquitto.org/debian/mosquitto-wheezy.list

sudo apt-get update
sudo apt-get dist-upgrade

WindowsPC側はChrome Appの MQTT Lens を立ち上げてパブリッシャー兼サブクライバーになってもらいます。先ほどインストールしたmosquittoはデフォルトでは認証の機能はOFFになっているようなので、すぐにMQTT Lensを使いテストをする事ができます。

MQTT サーバに接続後に適当な名前でトピックを購読した後、同じトピックでメッセージを発行するとMQTTサーバから配信されたメッセージを見ることができます。
MQTlens03
MQTlens04

トピックはディレクトリの様に”/”で区切って記述するとワイルドカードを使って購読の指定をすることもできるそうです。

ESP-WROOM-02の回路ですが前回と同じくikesato氏の「ESP-WROOM-02 の Arduino 環境で I2C 制御」に書かれている回路図を参考にさせていただきました。
回路図
この実験のためにESP-WROOM-02を追加購入しようとしたのですが、SWITCH SCIENCE社の完成モジュールは在庫が無く注文できませんでした。しかたなく「ESP-WROOM-02ピッチ変換済みモジュール《フル版》 ボードのみ」秋月電子通商 Wi-Fiモジュール ESP-WROOM-02を購入して半田付けをすることにしました。大した事は無いだろうと高を括っていたのですが、老眼で不器用な私には思いのほか難しかったです。

ブレッドボードに組ましたが、SWICH SCIENCE社のブレークアウトボードではESP-WROOM-02を挿すと幅いっぱいになってしまうので、写真のように内側にジャンパ線を使い配線しました。
ESP-WROOM-02_01

MQTTのArduinoライブラリの入手先はhttp://pubsubclient.knolleary.net/index.htmlこちらです。Download GitHubへ進み V2.4 (2016/01/15現在) Source code (zip) をダウンロードし解凍後 ArduinoIDEのライブラリズ ホルダへコピーします。

ArduinoIDEを起動してファイル-スケッチの例-PubSubClient-mqtt_8266 をロードしたのちに一部手直しです。

[変更部分]
const char* ssid = "........";     --> WiFiアクセス SSID
const char* password = "........"; --> 同上         パスワード
const char* mqtt_server = "broker.mqtt-dashboard.com"; --> MTQQサーバのipアドレス

pinMode(BUILTIN_LED, OUTPUT);   --> 4ヶ所(注釈含む) の "BUILTIN_LED" を"5"へ
/*
 Basic ESP8266 MQTT example

 This sketch demonstrates the capabilities of the pubsub library in combination
 with the ESP8266 board/library.

 It connects to an MQTT server then:
  - publishes "hello world" to the topic "outTopic" every two seconds
  - subscribes to the topic "inTopic", printing out any messages
    it receives. NB - it assumes the received payloads are strings not binary
  - If the first character of the topic "inTopic" is an 1, switch ON the ESP Led,
    else switch it off

 It will reconnect to the server if the connection is lost using a blocking
 reconnect function. See the 'mqtt_reconnect_nonblocking' example for how to
 achieve the same result without blocking the main loop.

 To install the ESP8266 board, (using Arduino 1.6.4+):
  - Add the following 3rd party board manager under "File -> Preferences -> Additional Boards Manager URLs":
       http://arduino.esp8266.com/stable/package_esp8266com_index.json
  - Open the "Tools -> Board -> Board Manager" and click install for the ESP8266"
  - Select your ESP8266 in "Tools -> Board"

*/

#include <ESP8266WiFi.h>
#include <PubSubClient.h>

// Update these with values suitable for your network.

const char* ssid = "WifiエスエスID";
const char* password = "同上password";
const char* mqtt_server = "MTQQサーバIPアドレス"; //

WiFiClient espClient;
PubSubClient client(espClient);
long lastMsg = 0;
char msg[50];
int value = 0;

void setup() {
  pinMode(5, OUTPUT);     // Initialize the 5 pin as an output
  Serial.begin(115200);
  setup_wifi();
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
}

void setup_wifi() {

  delay(10);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();

  // Switch on the LED if an 1 was received as first character
  if ((char)payload[0] == '1') {
    digitalWrite(5, LOW);   // Turn the LED on (Note that LOW is the voltage level
    // but actually the LED is on; this is because
    // it is acive low on the ESP-01)
  } else {
    digitalWrite(5, HIGH);  // Turn the LED off by making the voltage HIGH
  }

}

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect("ESP8266Client")) {
      Serial.println("connected");
      // Once connected, publish an announcement...
      client.publish("outTopic", "hello world");
      // ... and resubscribe
      client.subscribe("inTopic");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}
void loop() {

  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  long now = millis();
  if (now - lastMsg > 2000) {
    lastMsg = now;
    ++value;
    snprintf (msg, 75, "hello world #%ld", value);
    Serial.print("Publish message: ");
    Serial.println(msg);
    client.publish("outTopic", msg);
  }
}

MQTTlens にてTopicを “outTopic” にしてSubcribeすると 2秒おきにESP-WROOM-02の発した”hello world”を受信します。
teraterm2

また、Publish側のTopic を”InTopc”にしてメッセージを送信するとESP-WROOM-02に配信されます。
MQTT02
ESP-WROOM-02では受け取ったメッセージの先頭の1文字が”1″ならばLEDが点灯しそれ以外では消灯します。参照先のwebではopenHABを使用して操作する方法が書かれていましたが、今回は一応の目的を達することができたのでこれで良しとします。(すみません、この状態では「LEDチカチカ」を継続させるためには、非常に努力が必要ですね。タイトルに偽りありでした。ごめんなさい。)

(2016/01/15 追記) ブレッドボードの写真には写っていませんがUSBプラグとPCの間には下記のような電源供給できるHUBが入っています。直接PCにUSBケーブルを繋ぐのでは電源容量が不足する可能性があります。