ESP8266で指パッチン検出器を作る②

前回、FFTと相関係数で指パッチンを検出する仕組みを考えました。ところが、実際使ってみると何もしていないのに作動してしまうことがたまにあり、完璧ではありませんでした。

そこで今回は、誤作動しないような判定をSVM(サポートベクターマシン)で実現した話をご紹介します。

SVM(サポートベクターマシン)とは

SVMはデータの集合に対して、データを2つに分類するような境界線(面)を引く手法です。例によって、説明は適当です。詳しく知りたい方は「SVM」で検索すれば簡単に見つかると思います。

今回はこれを使って指パッチンデータとそれ以外のデータを分離したいと思います。分離が出来たら、得られた境界面を使って新たに得られたデータが指パッチンかそれ以外のどちらのグループに所属するか判定します。

SVMは学習データから分離面を求める時こそ多少計算を必要とするものの、一度分離面が得られたら新たなデータを判定するときは単純な計算ですむという利点があります。したがって学習だけパソコンで行えば、判定するときはESP8266でも十分です。

SVMを使って分離面を引く

前回は指パッチンの正解データだけを使いましたが、今回は指パッチンでないデータも必要です。前回同様にマイク入力をサンプリング周波数8000Hz、サンプル数1024のFFTにかけたデータ(512次元)を扱います。指パッチンしたときに出力されたデータと、それ以外の例えば手を叩いたときのデータをそれぞれ100データくらい用意しました。

早速SVMを使って分離面を引きたいと思います。SVMによる学習はpythonのライブラリであるscikit-learnを使いました。

環境を整えるのも面倒なので、Google Colaboratoryを使います。Google ColaboratoryはGoogleが提供しているWeb上でコーディングできるpython環境で、必要なパッケージも全て最初から入っているのでめちゃめちゃ簡単に開発を始められます。初めて使いましたが簡単すぎて驚きました。

scikit-learnでどうやってSVMをするかは下記のサイトがとても参考になりました。

https://data-science.gr.jp/implementation/iml_sklearn_svm.html

(ほとんど上記サイトのままですが、一応僕が使ったコードは後でgitか何かで公開する予定です。)

プログラムを走らせるとサポートベクトルとモデルの精度などの情報が出力されます。

得られた分離面を使ってデータを判定する

得られた分離面を使って、指パッチン判定を行っていきます。分離面といっても上記サイトの例ではRBFカーネルを用いた非線形分離を行っていますので、得られるのは面の式ではなく何個かのサポートベクトルになりますが、この辺の詳しい説明は割愛させていただきます(正直に言うと自分がまだ十分に理解していないからです笑)

とにかく、このサポートベクトルを使って新しいデータを判定するには、次の式に代入すれば良いとのことです。

サポートベクター分類の場合,以下の式を解くことで分類が出力される.上のパラメーターにおいて .support_vectors_ は以下の \(x_i\),.dual_coef_ は \(y_i\alpha_i\),.intercept_ は \(\rho\) である.

$$\begin{eqnarray}\operatorname{sgn}(\sum_{i=1}^{n}y_i\alpha_iK(x_i,x)+\rho)\tag{1}\end{eqnarray}$$

また,カーネルの中身は今回用いた RBF の場合,以下で計算されるが,このとき用いるのが .gamma であり,これは以下の \(\gamma\) である.すなわち,上のパラメーターさえ抽出すればものすごく簡単な計算で予測結果を得ることができる.これは別のプログラミング言語で書き換える際に便利.

$$K(x,x’)=e^{(-\gamma|x-x’|^2)}\tag{2}$$

上記サイトより引用

つまり、式(1)に学習によって得られたサポートベクトルと定数、マイクから拾ったデータを代入すれば、出てきた数値の符号によって指パッチンかそうでないかを判定できます。

この式をプログラムとして実装して、実際に判定するとこんな感じになります。

縦軸がSVMの出力、横軸はデータのindex。ある程度大きい音がしたときのみ判定。

グラフは作ったSVMを実行しながら何回か物音を立ててみた結果です。マイク入力に対して常にFFT→SVMを行うと計算が追いつかないので、ある程度大きい音がしたときだけ判定を行うようにしています。

見て分かる通り、指パッチンしたときだけSVMの出力が正になっていることがわかります。自分で言うのもなんですが、かなりいい精度で検出できます。手や硬いものをぶつけても検出しませんし、後ろで音楽を流していてもちゃんと検出してくれます。たまに指パッチンしても検出しないときがありますが、何もしていないときに誤検出してしまうことはありませんでした。

指パッチン検出器、完成です!
あとはこれを使って色々なアクションを起こすデバイスを作っていく予定です。

最後までご覧いただきありがとうございました!
こんなことできたらいいな、というアイデアがありましたら是非コメントで教えて下さい。

ESP8266で指パッチン検出器を作る①

最近私生活がバタバタしていたので久しぶりの投稿です。ちょっと前から考えていた指パッチンで何かアクションを起こすデバイスが試行錯誤を重ねようやく完成したので、作り方をご紹介しようと思います。

今回の目標

今回の目標は「指パッチンしたら何か起こる」デバイスを作ることです。

  • 指パッチン「だけ」を検出して何かアクションを起こす。手を叩いたり何かがぶつかる音では反応しない。
  • できるだけ誤作動(指パッチンしていないのにアクション)しない。
  • コストと消費電力を抑えるため、ラズパイではなくESP8266を使う。そのため、C++でプログラムを書き、しかもコード量を減らす必要がある。

家に帰ってきたら指を鳴らしてカッコよく電気をつけたい。そんな要望に当システムがお応えします。

指パッチンの音を解析する

指パッチンの音の特徴は?

指パッチンだけを検出するためには、指パッチンの音の特徴を捉える必要があります。マイクで取った音の信号だけでは、データがバラバラすぎて判定できません。

フーリエ変換してみる

指パッチンの音と手を叩く音は、同じ単発音ですが、人間の耳には音色の違いがはっきり分かります。その理由は音に含まれる周波数の配合が異なるためです。

そこでフーリエ変換を使って周波数分析を行います。周波数分析とは一定時間の音の時系列データの中に、各周波数成分がどのくらい入っているかを調べる手法です(ざっくり)。フーリエ変換を行うと、時系列データ(横軸が時間、縦軸が振幅)が周波数成分に変換されて、横軸が周波数、縦軸がパワースペクトルになります。

周波数分析を使えば指パッチンと手を叩く音で異なる結果が得られるはずです。
指パッチン、手を叩く、金属をぶつけるの3種類の音を周波数分析した結果がこちらです。

3種類の音をフーリエ変換した結果。指パッチン同士はなんとなく似てそう。

なんとなく指パッチンは似ている波形が出ています。逆にそれ以外は全く違う周波数的特徴を持っていることがわかります。次はこの指パッチンとそれ以外のデータを分類する判定アルゴリズムを作っていきます。

指パッチンの判定

周波数分析が出来たとして、どういう波形なら指パッチンと判断するかを決める必要があります。そのためには指パッチンに近いかどうかを定量的に評価する指標が必要です。評価の方法としては色々ありますが、相関係数を使う方法とサポートベクターマシンを使う方法を思いついたのでやってみました。

相関係数を使って判定

得られたデータがどれくらい指パッチンに似ているか定量的にわかればいいので、まず相関係数を使う方法を思いつきました。相関係数は2つのデータ(ベクトル)が似ているかどうかを数値で表すことが出来ます。

まず指パッチンを何回か録音して、指パッチンの平均的なデータを求めておきます。実際に判定するときは、この平均データと新たに得られたデータを比較することによって、得られたデータがどのくらい指パッチンに似ているか判定します。

指パッチンを20回録音して得られた平均データがこちらです。

指パッチン20回の平均データ。これを基準に相関係数を求める。

これと新たに得られたデータを比較して、相関係数が高ければ指パッチンと判定します。以下は指パッチンの平均データをx軸、新たに得られたデータをy軸にとった散布図です。教科書で見たことあるようなきれいなグラフになりました笑。

平均をデータに近い=相関が強い

相関係数はデータが似ているほど1に近くなります。逆に全く相関がなければ0になります。指パッチンと指パッチン以外の約100回のデータで実際に判定を行ってみたところ、次のようになりました。

約100回のデータに対して相関係数を計算。指パッチンはちゃんと相関係数が大きくなっている。

これを見ると、指パッチンのときはだいたい0.4以上の相関係数が出ていることがわかります。

これをトリガーにすれば実用的な判定システムが作れそうです。

長くなってきたので今回はここまでにします。
次回に続きます。

次回→相関係数で判定するデメリットとSVM(サポートベクターマシン)

ESP8266で時刻取得ができない

ESPで実行ログに現在時刻をつけて確認したいと思い、ネットから時刻を取得する方法を調べてみました。

こちらで紹介されているコードをそのまま使ってみて、ちゃんと取得できたので自分のプログラムに入れてみたのですが、なぜか取得できませんでした。

取得できない原因

原因は意外なところにあって、WiFi接続を固定IPアドレスにしていたからでした。ESPWiFiはIPアドレスやゲートウェイの設定はあってもDNSサーバーを設定する方法が無いらしく、固定IPだけ設定すると名前解決ができなくて取得に失敗するっぽいです。

  (中略)
  /* 
   *  固定IPに設定していると、名前解決できない!(=ドメイン名でアクセスできない)
   */
  IPAddress ip(192,168,1,33);
  IPAddress gateway(192,168,1,1);
  IPAddress subnet(255,255,255,0);
  WiFi.config(ip, gateway, subnet);
  
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while(WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(500);
  }
  Serial.println();
  Serial.printf("Connected, IP address: ");
  Serial.println(WiFi.localIP());

解決方法

解決方法としては、mDNSとかいうライブラリを使って名前解決できるようにするか、簡易的には以下のようにNTPサーバーをIPアドレスで指定すればOKです。

(中略)
//configTime( JST, 0, "ntp.nict.jp", "ntp.jst.mfeed.ad.jp");
// 名前解決できないときは直接IPアドレスでNTPサーバーを指定
configTime( JST, 0, "133.243.238.244");

これだけ。簡単。
本当はちゃんとDNS使いたいけど今の環境だと色々難しいので早く回線契約したい。

帰宅したら自動で解錠するスマートロックを自作する④~指紋認証編~

前回、タクトスイッチを使って外から解錠するやつを作りました。今回はいよいよスマートロックっぽいことをしていきます。なんと指紋認証に挑戦です。意外と簡単に、かつ安くできましたのでご紹介します。

作りたいもの

前回 のつづきです。ICカードで解錠はいったん置いておきます。(作ったけど、記事にするのが面倒くさい。。。)

今回の目標は、指でセンサにふれると解錠してくれるスマートロックを作ることです。

  1. ドアが閉まったら自動で施錠
  2. 暗証番号で解錠
  3. ICカードで解錠
  4. 指紋認証で解錠
  5. スマホを持って近づくだけで解錠

前回から追加で必要なもの

  • 指紋モジュールDY50 1200円くらい

Amazonで最安だった指紋認識モジュールです。高いやつだと4000円くらいするのにこちらは1200円なのでちょっと不安ですが、きちんと認識してくれます。若干認識までにかかる時間が長いかも?。指を置いてから0.5~1秒くらいかかります。

導線にピンヘッダをはんだ付けする

指紋モジュールから出ている線はVCC、GND、TX、RXの四本です。これをArduinoで接続するためにピンをはんだ付けします。

(以下、作業途中の写真を撮るのを忘れたのでイメージ画像です。)

モジュール付属のコネクタ・ケーブルを半分に切断して、ジャンパワイヤとピンをはんだ付けしていきます。

付属のケーブルを半分に切断
ケーブル+ジャンパワイヤ+ピンをはんだ付け。

ちなみに、モジュールの線の配置はこのようになっています。

ESP8266に接続

UART接続でESP8266と通信しますので、以下のように接続します。マイコンとの通信はソフトウェアシリアルを使うので、TXとRXをESPのD2とD3につなぎます。(追記:僕が使っているESP8266ではソフトウェアシリアルが使えませんでした!後述します。)

DY50ESP8266
VCCVin
TXD2
RXD3
GNDGND

指紋認識テスト

モジュールが接続できたら指紋認証がうまくいくかテストしてみます。ArduinoIDEのライブラリ管理から検索するか、adafruitのgitからモジュール用のドライバとサンプルプログラムをダウンロードします。

ArduinoIDEからサンプルプログラムを開いてenrollを書き込んでみます。これだけで指紋の登録ができる…と色々なサイトに書いてありますが、僕の場合は上手くいきませんでした…。プログラムを走らせてシリアルモニタを開いても「sensor not found.」と表示され、そもそも何も通信できていないようです。

色々調べた結果、原因はESP8266モジュールのソフトウェアシリアルが怪しいと結論づけました。ESP8266はデフォルトの通信レートが119200bpsであるのに対し、このモジュールは9600bpsで通信するので上手くいっていないのだと思います。ネットの記事を調べてESP8266の通信レートを9600に落とす方法を試しましたが、僕の持っているモジュールでは変更しても電源を入れ直すと通信レートが戻ってしまい上手く行きませんでした。

どうするか

ESP8266でソフトウェアシリアルが使えないならどうするか、これに結構悩みました。

案① Arduino UnoとかNanoとか他のモジュールを使う。

ただし、WiFiから解錠したり他の機器との連携は無理。NanoとESPを両方使えばできるがなんか無駄な気がする。

案② ESP8266のハードウェアシリアルを使う。

ただし、シリアルモニタが使えなくなるためデバッグができなくなる。ログをWiFi経由で確認できるようにすれば一応デバッグできる。

3日くらい悩んでましたが、今後WiFiはやっぱり使いたいのと、これまで作ってた基盤がESP用になっているのでまた1から基盤にはんだ付けし直すのが面倒という理由で案②を採用することにしました。

でもこの方法だとシリアルモニタからモジュールに数値を送信することができないんですよね…。指紋の登録時には番号を送信する必要があるので、指紋登録時は①、運用時は②という面倒くさいことになっております。

指紋を登録してみる

指紋登録だけはArduino Nanoを使って行います。adafruitのサンプルプログラムからenrollを書き込んで、指紋モジュールのTXとRXをArduinoのD2とD3につなぎます。

シリアルモニタを開いて、メッセージ通りに番号を入力して指を2回置けば登録完了です。

登録した指紋は電源を切っても保持されているので、よく使う指を全部登録したら次はESPにつないで認証できるかやってみます。

指紋を認識できるかテストしてみる

ここからはモジュールをESP8266につないで作業していきます。ソフトウェアシリアルではなくハードウェアシリアルを使うので接続はモジュールのTX、RXとESPのRX、TXです。また、プログラムの書き込みを行う際はモジュールを外さないと書き込めません。それとシリアルモニタは使えません。

書き込むプログラムはサンプルプログラムfingerprintをちょっと変えるだけでOKです。

// Adafruitのサンプルプログラムfingerprintをハードウェアシリアルに改変
// Serial.printlnは全部コメントアウトしています。

#include <Adafruit_Fingerprint.h>

const int led_pin = 16;  // LEDはD0に接続

// On Leonardo/Micro or others with hardware serial, use those! #0 is green wire, #1 is white
// uncomment this line:
// #define mySerial Serial1

// For UNO and others without hardware serial, we must use software serial...
// pin #2 is IN from sensor (GREEN wire)
// pin #3 is OUT from arduino  (WHITE wire)
// comment these two lines if using hardware serial
//SoftwareSerial mySerial(2, 3);  // ハードウェアシリアルを使うのでコメントアウト

Adafruit_Fingerprint finger = Adafruit_Fingerprint(&Serial);  // ハードウェアシリアル

void setup()  
{
  //Serial.begin(9600);
  //while (!Serial);  // For Yun/Leo/Micro/Zero/...
  //delay(100);
  //Serial.println("\n\nAdafruit finger detect test");

  // set the data rate for the sensor serial port
  finger.begin(57600);
  
  if (finger.verifyPassword()) {
    //Serial.println("Found fingerprint sensor!");
  } else {
    //Serial.println("Did not find fingerprint sensor :(");
    while (1) { delay(1); }
  }

  finger.getTemplateCount();
  //Serial.print("Sensor contains "); Serial.print(finger.templateCount); Serial.println(" templates");
  //Serial.println("Waiting for valid finger...");

  pinMode(led_pin, OUTPUT);
}

void loop()                     // run over and over again
{
  int num = getFingerprintIDez();

  // 認証成功したらLEDを光らせる
  if( num > 0 ){
    digitalWrite(led_pin, HIGH);
    delay(1000);
    digitalWrite(led_pin, LOW);
  }
  delay(50);            //don't ned to run this at full speed.
}

uint8_t getFingerprintID() {
  uint8_t p = finger.getImage();
  switch (p) {
    case FINGERPRINT_OK:
      //Serial.println("Image taken");
      break;
    case FINGERPRINT_NOFINGER:
      //Serial.println("No finger detected");
      return p;
    case FINGERPRINT_PACKETRECIEVEERR:
      //Serial.println("Communication error");
      return p;
    case FINGERPRINT_IMAGEFAIL:
      //Serial.println("Imaging error");
      return p;
    default:
      //Serial.println("Unknown error");
      return p;
  }

  // OK success!

  p = finger.image2Tz();
  switch (p) {
    case FINGERPRINT_OK:
      //Serial.println("Image converted");
      break;
    case FINGERPRINT_IMAGEMESS:
      //Serial.println("Image too messy");
      return p;
    case FINGERPRINT_PACKETRECIEVEERR:
      //Serial.println("Communication error");
      return p;
    case FINGERPRINT_FEATUREFAIL:
      //Serial.println("Could not find fingerprint features");
      return p;
    case FINGERPRINT_INVALIDIMAGE:
      //Serial.println("Could not find fingerprint features");
      return p;
    default:
      //Serial.println("Unknown error");
      return p;
  }
  
  // OK converted!
  p = finger.fingerFastSearch();
  if (p == FINGERPRINT_OK) {
    //Serial.println("Found a print match!");
  } else if (p == FINGERPRINT_PACKETRECIEVEERR) {
    //Serial.println("Communication error");
    return p;
  } else if (p == FINGERPRINT_NOTFOUND) {
    //Serial.println("Did not find a match");
    return p;
  } else {
    //Serial.println("Unknown error");
    return p;
  }   
  
  // found a match!
  //Serial.print("Found ID #"); Serial.print(finger.fingerID); 
  //Serial.print(" with confidence of "); Serial.println(finger.confidence); 

  return finger.fingerID;
}

// returns -1 if failed, otherwise returns ID #
int getFingerprintIDez() {
  uint8_t p = finger.getImage();
  if (p != FINGERPRINT_OK)  return -1;

  p = finger.image2Tz();
  if (p != FINGERPRINT_OK)  return -1;

  p = finger.fingerFastSearch();
  if (p != FINGERPRINT_OK)  return -1;
  
  // found a match!
  //Serial.print("Found ID #"); Serial.print(finger.fingerID); 
  //Serial.print(" with confidence of "); Serial.println(finger.confidence);
  return finger.fingerID; 
}

変更点としてはmySerialの部分をSerialに変えて、Serial.printlnをすべてコメントアウトするだけです。

シリアルモニタが使えないので、確認用のLEDをD0とGNDにつけます。認証が成功するとLEDを1秒間光らせます。

認証成功で解錠する

前回 のプログラムの解錠条件に、今回の指紋認証の結果を加えるだけです。最終的なプログラムを載せておきます。

#include <Servo.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
//#include <wifi_setting.h>
#include <Adafruit_Fingerprint.h>
#include <MyFunctions.h>

// ルータ設定
IPAddress ip(192,168,100,102);
IPAddress gateway(192,168,100,1);
IPAddress subnet(255,255,255,0);

// ESP8266のピン番号
const int servo_pin = 2;
const int door_pin = 12;
const int button_pin = 14;

Servo myservo;             // サーボオブジェクトを生成
int door_sensor = LOW;
int door_sensor_last = LOW;
ESP8266WebServer server(80);  //サーバーオブジェクト
Adafruit_Fingerprint finger = Adafruit_Fingerprint(&Serial);  // ハードウェアシリアル
LogBuffer action_log(64);

void door_open(){
  myservo.attach(servo_pin);
  delay(50);
  myservo.write(170);
  delay(500);
  myservo.write(90);
  delay(500);
  myservo.detach();
}

void door_close(){
  myservo.attach(servo_pin);
  delay(50);
  myservo.write(10);
  delay(500);
  myservo.write(90);
  delay(500);
  myservo.detach();
}

void handle_open(){
  door_open();
  server.send(200, "text/html", "opened");
}

void handle_close(){
  door_close();
  server.send(200, "text/html", "closed");
}

void handle_log(){
  server.send(200, "text/html", action_log.to_HTML());
}

bool secret_command(){
  // HIGHがスイッチが押されていない状態
  // LOWがスイッチが押されている状態
  
  unsigned long pressed_time = millis();
  unsigned long released_time = millis();
  int now_state = LOW;
  int last_state = LOW;
  
  float button_lpf = 0.0;  // センサ信号にローパスフィルタ(LPF)をかけた値
  
  String key = "---.--..-.";  //モールス符号でO・P・E・N
  String input = "";
  while(millis() - released_time < 3000){

    button_lpf = 0.9 * button_lpf + 0.1 * digitalRead(button_pin);
    if( button_lpf > 0.8 ){
      now_state = HIGH;
    }else if( button_lpf < 0.2 ){
      now_state = LOW;
    }

    if( last_state == HIGH && now_state == LOW ){
      // ボタンが押されたとき、HIGH→LOW
      pressed_time = millis();
    }else if( last_state == LOW && now_state == HIGH){
      // ボタンが離されたとき、LOW→HIGH
      released_time = millis();

      // ボタンを押していた時間で「.」か「-」を追加
      input += (released_time - pressed_time < 300) ? "." : "-";
    }

    last_state = now_state;
    delay(1);
  }
  
  if(key == input){
    return true;
  }else{
    return false;
  }
  
}

int getFingerprintIDez() {
  uint8_t p = finger.getImage();
  if (p != FINGERPRINT_OK)  return -1;

  p = finger.image2Tz();
  if (p != FINGERPRINT_OK)  return -1;

  p = finger.fingerFastSearch();
  if (p != FINGERPRINT_OK)  return -1;
  
  // found a match!
  //action_log.add("Found ID #"); action_log.add(finger.fingerID); 
  //action_log.add(" with confidence of "); action_log.add(finger.confidence);
  return finger.fingerID; 
}

void setup() 
{
  //固定IPで運用するときの設定
  WiFi.config(ip, gateway, subnet);
  //WiFi.begin(WIFI_SSID, WIFI_PWD);

  // WiFiに接続するまで待つ
  action_log.add("");
  while(WiFi.status() != WL_CONNECTED){
    delay(1000);
    action_log.add(".");
  }
  
  action_log.add("");
  action_log.add("Connected!");
  action_log.add("IP Address: ");
  action_log.add(WiFi.localIP());

  // Webサーバを設定
  server.on("/open", handle_open);
  server.on("/close", handle_close);
  server.on("/log", handle_log);
  server.begin();

  // 開閉センサの入力を内部プルアップにする
  pinMode(door_pin, INPUT_PULLUP);

  // サーボ変数をピンに割り当て
  myservo.attach(servo_pin);
  door_close();

  // 指紋センサ
  finger.begin(57600);
  
  if (finger.verifyPassword()) {
    action_log.add("Found fingerprint sensor!");
  } else {
    action_log.add("Did not find fingerprint sensor :(");
    while (1) { delay(1); }
  }

  finger.getTemplateCount();
  action_log.add("Sensor contains "); action_log.add(finger.templateCount); action_log.add(" templates");
  action_log.add("Waiting for valid finger...");
} 

void loop() 
{ 
  // サーバとして待ち受ける
  server.handleClient();

  // 現在のドアの開閉を検知
  // LOW=閉
  // HIGH=開
  door_sensor = digitalRead(door_pin);

  // 「開」→「閉」になったタイミングでサーボモータを回す
  if(door_sensor == LOW && door_sensor_last == HIGH){
    delay(1000);
    door_close();
  }  
  door_sensor_last = door_sensor;

  // ボタンで解錠
  if(digitalRead(button_pin) == LOW){
    bool success = secret_command();
    
    if(success){
      door_open();
    }
  }

  // 指紋で解錠
  if( getFingerprintIDez() > 0 ){
    door_open();
    action_log.add("Door opened.");
  }
  
  delay(10);
}

シリアルモニタが使えないのでWiFiからログを確認できる機能を追加しています。何かメッセージを出したいときはaction_logに保存しておき、http://192.168.xxx.xxx/logで表示します。action_logのクラスはMyFunction.hに別ファイルとして書いてます。

以上です。

(追記予定)

帰宅したら自動で解錠するスマートロックを自作する②~ボタンで解錠編~

前回はドアが閉まったら自動で鍵をかけるシステムを作りましたが、それだと開けることができないので結局鍵を出さないといけなくて面倒です。そこで今回は鍵の代わりにタクトスイッチを使った暗証番号的な方法で解錠する方法をご紹介します。

作りたいもの

前回のつづきです。

  1. ドアが閉まったら自動で施錠
  2. 暗証番号で解錠
  3. ICカードで解錠
  4. スマホを持って近づくだけで解錠

前回から追加で必要なもの

  • タクトスイッチ
押すと導通するふつうのタクトスイッチ

ドアの外側にスイッチをつける

外から押せる位置にスイッチをつけます。うちのドアの横のすき間から線を通せたので普通の導線を使っています。ドアのすき間が狭い場合はフラットケーブル等を使うと良いと思います。ESP8266側はGNDとデジタル入出力に繋ぎます。

方法を決める

ボタンを準備したら次はどんなパターンでボタンを押したら解錠するか決めます。

やり方はみなさんのオリジナリティを出せるところだと思います。例えばボタンを押す順番や、押す時間の長短などを組み合わせて秘密の暗号にします。ここでは長短の組み合わせをパスワードにしてみます。

長短の組み合わせ(モールス符号)をパスワードにしてみる

皆さんご存じモールス符号をパスワードにしてみます。例えば「OPEN」のモールス符号で「--- ・--・ ・ ―・」と押した時に解錠するようにしてみます。

新しく追加したのは関数secret_command()の部分です。ボタンを押したり離したりしたときに、digitalReadの値の立ち下がり・立ち上がりを検出して、ボタンを押していた時間で「・」か「―」を判定します。3秒以上入力がなかったら終了して、入力された符号とパスワードが一致していたら解錠します。

#include <Servo.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>

// ルータ設定
IPAddress ip(192,168,100,102);
IPAddress gateway(192,168,100,1);
IPAddress subnet(255,255,255,0);

// ESP8266のピン番号
const int servo_pin = 2;
const int door_pin = 12;
const int button_pin = 14;

Servo myservo;             // サーボオブジェクトを生成
int door_sensor = LOW;
int door_sensor_last = LOW;
ESP8266WebServer server(80);  //サーバーオブジェクト


void door_open(){
  myservo.attach(servo_pin);
  delay(50);
  myservo.write(170);
  delay(500);
  myservo.write(90);
  delay(500);
  myservo.detach();
}

void door_close(){
  myservo.attach(servo_pin);
  delay(50);
  myservo.write(10);
  delay(500);
  myservo.write(90);
  delay(500);
  myservo.detach();
}

void handle_open(){
  door_open();
  server.send(200, "text/html", "opened");
}

void handle_close(){
  door_close();
  server.send(200, "text/html", "closed");
}

bool secret_command(){
  // HIGHがスイッチが押されていない状態
  // LOWがスイッチが押されている状態
  
  unsigned long pressed_time = millis();
  unsigned long released_time = millis();
  int now_state = LOW;
  int last_state = LOW;
  
  float button_lpf = 0.0;  // センサ信号にローパスフィルタ(LPF)をかけた値
  
  String key = "---.--..-.";  //モールス符号でO・P・E・N
  String input = "";
  while(millis() - released_time < 3000){

    button_lpf = 0.9 * button_lpf + 0.1 * digitalRead(button_pin);
    if( button_lpf > 0.8 ){
      now_state = HIGH;
    }else if( button_lpf < 0.2 ){
      now_state = LOW;
    }

    if( last_state == HIGH && now_state == LOW ){
      // ボタンが押されたとき、HIGH→LOW
      pressed_time = millis();
    }else if( last_state == LOW && now_state == HIGH){
      // ボタンが離されたとき、LOW→HIGH
      released_time = millis();

      // ボタンを押していた時間で「.」か「-」を追加
      input += (released_time - pressed_time < 300) ? "." : "-";
    }

    last_state = now_state;
    delay(1);
  }
  
  if(key == input){
    return true;
  }else{
    return false;
  }
  
}

void setup() 
{ 
  
  Serial.begin(115200);
  delay(200);

  //固定IPで運用するときの設定
  WiFi.config(ip, gateway, subnet);
  WiFi.begin(WIFI_SSID, WIFI_PWD);

  // WiFiに接続するまで待つ
  Serial.println("");
  while(WiFi.status() != WL_CONNECTED){
    delay(1000);
    Serial.print(".");
  }
  
  Serial.println("");
  Serial.println("Connected!");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());

  // Webサーバを設定
  server.on("/open", handle_open);
  server.on("/close", handle_close);
  server.begin();

  // 開閉センサの入力を内部プルアップにする
  pinMode(door_pin, INPUT_PULLUP);

  // サーボ変数をピンに割り当て
  myservo.attach(servo_pin);

  door_close();
} 

void loop() 
{ 
  // サーバとして待ち受ける
  server.handleClient();

  // 現在のドアの開閉を検知
  // LOW=閉
  // HIGH=開
  door_sensor = digitalRead(door_pin);

  // 「開」→「閉」になったタイミングでサーボモータを回す
  if(door_sensor == LOW && door_sensor_last == HIGH){
    delay(1000);
    door_close();
  }  
  door_sensor_last = door_sensor;

  // ボタンで解錠
  if(digitalRead(button_pin) == LOW){
    bool success = secret_command();
    
    if(success){
      door_open();
    }
  }
  
}

完成!

簡単ではありますがこれだけでパスワード解錠的な機能をつけることができました。他にもテンキーなどのデバイスを付けてパスワードと一致したら解錠とかもできると思います。もっといいやり方があるよ!という方はコメントで教えてください。

ちゃんとセキュリティを考えるなら、10回入力に失敗したら一定時間操作を受け付けなくなるとかも考えたほうが良いかもしれません。今回の例ならパスワードが10ケタなので、2の10乗=1024回試されたら破られます。もう少しパスワードは長いほうが良いかも。

←前回    次回→

ArduinoでFFTを使う

マイクから拾った音の音階を調べるプログラムのテストです。Arduinoだけを使ってFFTする方法を調べてみたのですが、情報が古く、日本語では欲しい情報になかなかたどり着けなかったので上手く行ったやり方を紹介します。

FFTとは

一定期間の音信号を解析して、どの周波数成分がどの程度含まれているか求める手法です。信号の数を2のべき乗にすることによって高速に計算することが出来ます。

あんま上手く説明できません。とりあえず、鳴っている音の周波数が何なのかわかる手法です。

使うもの

  • ESP8266(手持ちのArduino nanoでもやってみましたが、メモリが足りずスケッチが書き込めませんでした。他のArduinoは要検証です。)。以下の2個セットのやつが値段的にも最安でWiFiも使えてメモリも多いのでよく買ってます。
  • マイクモジュール

やり方

ここのやり方に沿って進めていくだけですが、かいつまんで解説していきます。
https://www.norwegiancreations.com/2017/08/what-is-fft-and-how-can-you-implement-it-on-an-arduino/

ライブラリをインストール

Arduino IDEを開き、スケッチ→ライブラリをインクルード→ライブラリを管理…の順にクリックします。

スケッチ→ライブラリをインクルード→ライブラリを管理… の順にクリック

検索バーに「fft」を入力し、「arduinoFFT」をインストールします。

arduinoFFTをインストール

これだけで完了です。

プログラム書き込み

下にサンプルプログラムを置いておきます。上記サイトに掲載されているプログラムほぼそのままです。一部ESP8266向けに変更しています。オリジナルよりもサンプル数とサンプリング周期を上げています。

FFTの特徴として、サンプリング周波数の半分の周波数までしか解析することが出来ません。サンプリング周波数が8000Hzなら、4000Hz以上の音は検出できません。

また、サンプル数を多くすると、結果の分解能が上がる代わりに測定にかかる時間が増えます。サンプル数は2のn乗にしなければならないと決まっていますので、欲しい分解能に応じて決める必要があります。

分解能はサンプリング周波数[Hz]÷サンプル数になります。つまり下のサンプルプログラムでは8000÷256=32[Hz]刻みの結果しか得られないことになります。判別する周波数の精度を上げたい場合にはサンプル数(=サンプリング時間)をあげる必要があります。

下記のプログラムでは最も成分の多い周波数(ピーク周波数)を出力するようにしています。

#include "arduinoFFT.h"
 
#define SAMPLES 256             //Must be a power of 2
#define SAMPLING_FREQUENCY 8000 //Hz, must be less than 10000 due to ADC
 
arduinoFFT FFT = arduinoFFT();
 
unsigned int sampling_period_us;
unsigned long microseconds;
 
double vReal[SAMPLES];
double vImag[SAMPLES];
 
void setup() {
    Serial.begin(115200);
 
    sampling_period_us = round(1000000*(1.0/SAMPLING_FREQUENCY));
}
 
void loop() {
   
    /*SAMPLING*/
    for(int i=0; i<SAMPLES; i++)
    {
        microseconds = micros();    //Overflows after around 70 minutes!
     
        vReal[i] = analogRead(A0);  //ESP8266の場合は「A0」。普通のArduinoは「0」。
        vImag[i] = 0;
     
        while(micros() < (microseconds + sampling_period_us)){
        }
    }
 
    /*FFT*/
    FFT.Windowing(vReal, SAMPLES, FFT_WIN_TYP_HAMMING, FFT_FORWARD);
    FFT.Compute(vReal, vImag, SAMPLES, FFT_FORWARD);
    FFT.ComplexToMagnitude(vReal, vImag, SAMPLES);
    double peak = FFT.MajorPeak(vReal, SAMPLES, SAMPLING_FREQUENCY);
 
    /*PRINT RESULTS*/
    Serial.println(peak);     //Print out what frequency is the most dominant.
 
    for(int i=2; i<(SAMPLES/2); i++)
    {
        /*View all these three lines in serial terminal to see which frequencies has which amplitudes*/
         
        //Serial.print((i * 1.0 * SAMPLING_FREQUENCY) / SAMPLES, 1);
        //Serial.print(" ");
        //Serial.println(vReal[i], 1);    //View only this line in serial plotter to visualize the bins
    }
 
    delay(1);  //Repeat the process every second OR:
    //while(1);       //ESPだと無限ループがエラーになるので注意
    
}

Arduinoとマイクを接続

わざわざ書くまでもありませんが、Arduinoとマイクを接続します。3.3VとGNDとA0をマイクモジュールにつなぐだけです。

マイクモジュールを使えば簡単!

テスト

プログラムを書き込んだらシリアルプロッタを開いて口笛を吹いてみます。

口笛が下手

口笛の音階があっているかはさておき、音の高さを判別することができました!

追記:電源を安定化すれば精度が上がる

上記の方法で問題なくFFTはできるのですが、たまに誤差が多い結果が出ることもありました。安いマイクなのでしょうがないのかな?と思っていましたが、ふと思いついて電源を見直したところ精度がめっちゃ向上したので報告します。

どうやらArduinoの3.3Vピンから電源を取るとノイズが多いようで、マイクの出力がノイズを含んでいました。

マイクの電源を別に用意したところ、ノイズがなくなりFFTの出力もキレイになりました。

マイク用電源を別に追加

電池を別に用意するのが面倒な場合は、電源安定回路やレギュレータを使うのもありです。esp8266のVinピンから取っても大丈夫でした。(VinはUSBの5Vと繋がっている)

今回は以上です。

【キルミーベイベー】あぎりさんの部屋にある装置をつくってみた

はじめに

キルミーベイベー第6話にて…

やすな「からくりっぽいのとかは無いんですか?」
あぎりさん「ありますよ~」
「この置物を動かすと~
置物(ヤッベー)
「クーラーがつきます♪」

あぎりさんの部屋にあった置物のイメージ

何この装置…欲しい…
ということで今回はこれを作っていこうと思います。

基本構想

  • エアコンは赤外線を使ってつける
  • 赤外線送信機と置物は分けて作る
  • 赤外線送信機と置物はWiFi通信でつなぐ
  • 置物を動かしたらWiFiで送信機に指令を送る→送信機がエアコンをつける

赤外線送信機と置物は一体で作ることもできますが(そっちのほうが安いし簡単)、どうしても赤外線がエアコンに届くように配置を工夫する必要がありますし、置物の正面に立つと使えなかったりするのでここはWiFiを使っています。
それにLEDむき出しだとなんか凄さが半減して嫌じゃないですか(笑)

作りたい装置のイメージ

必要なもの

  • 適当な置物
  • 回転センサ(10kΩ可変抵抗器、10kでなくても可)
  • 10kΩ抵抗器(アナログ入力が最大1Vの場合のみ必要。可変抵抗の2倍)
  • ESP8266モジュール(NodeMCU ESP8266 Board)×2
  • 赤外線LED
  • トランジスタ(2CS1815)
  • 200Ω抵抗器(あったほうがいいけどなくても可)

まず置物側ですが、置物本体に回転によって抵抗値が変わる可変抵抗器を使いたいと思います。可変抵抗器は別名ボリュームともいい、昔のテレビやラジオのダイヤルやアンプの音量調節など、ダイヤル状のものにはまず使われているであろう基本的な部品です。これを使って置物が一定以上回ったらWiFi信号を送るデバイスを作ろうと思います。
可変抵抗と普通の抵抗は10kΩと書きましたが別に1kΩでもいいです。

次に送信機側ですが、赤外線LEDの他に抵抗とトランジスタを使って赤外線が最大限届くようにします。

必要なものリスト(AMAZON)

使う部品リストを載せておきます。ただ、抵抗やトランジスタはAmazonだとバラ売りされていないので、お近くの電子部品の販売店に行かれる方が絶対良いです。東急ハンズとかでも売ってます。逆にESP8266モジュールはネットで買った方が安くておすすめです。

置物側の設計

置物を回転させたらWiFiを通じてリクエストを送信する装置を作ります。

必要なものは、ESP8266と回転によって抵抗が変化する可変抵抗、確認用のLEDです。

回転を検知する可変抵抗

可変抵抗とESP8266モジュールのつなぎ方

可変抵抗の仕組みは簡単です。両端に電圧をかけると、(抵抗が一様ならば)その間で電圧は一様に降下します。可変抵抗を回転させるとこの抵抗のどこを触るかが変えられるので、電圧をESP8266で読み取るだけで回転量が分かります。

可変抵抗の仕組み

アナログ入力の上限が1VのESP8266を使う場合

僕のESP8266モジュールはアナログ入力に0~3.3Vが使えるのですが、アナログ入力が0~1VのESP8266もあります。そういう場合は次のように20kΩの抵抗を入れることによって出力の範囲を変えることが出来ます。

出力の範囲を変える方法

組み立て

100均の木箱のフタを利用しました
見た目がスッキリするように
適当な置物をくっつけて完成!

Arduinoソースコード(置物側)

回転を検知→HTTP GETリクエストを送信します。

/* 
 *  あぎり「この置物を動かすと~
 *  クーラーが付きます♪
 *  (具体的な動作:可変抵抗の値に応じてHTTPリクエストを送る)
 */
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266HTTPClient.h>
#include <mDNSResolver.h>       //https://github.com/madpilot/mDNSResolver

#define WIFI_SSID "********"
#define WIFI_PWD "****"
#define TARGET_HOSTNAME "ir-sender.local"
String host_ip = "";


// 前回送ったコマンドを記憶
#define STATE_OFF 0
#define STATE_ON 1
int state = STATE_OFF;

// ONまたはOFFのしきい値
// 置物を回したときのセンサ値を見て、いい感じの値に設定
#define ON_THRESH 220
#define OFF_THRESH 240

// 赤外線LEDの+側
#define LED_PIN 14

// 名前解決してくれる(ir-sender.local → 192.168.0.22)
WiFiUDP udp;
mDNSResolver::Resolver resolver(udp);

String ip_to_string(IPAddress ip){
  return 
  String(ip[0]) + String(".") +
  String(ip[1]) + String(".") +
  String(ip[2]) + String(".") +
  String(ip[3]);
}

String getPageSource(String host) {
  HTTPClient http;
  
  http.setTimeout(500);
  http.begin(host);
  
  int httpCode = http.GET();
  
  String result = "";

  if (httpCode < 0) {
    result = http.errorToString(httpCode);
  } else if (http.getSize() < 0) {
    result =  "size is invalid";
  } else {
    result = http.getString();
  }

  http.end();
  return result;
}

void wake_wifi(){
  // 原因未解明!
  // WiFi接続が確認できるまで待つ。これがないとGETが失敗することがある。
  while(WiFi.status() != WL_CONNECTED){
    delay(100);
    Serial.print(".");
  }
}

String get_ip(char* host){
  IPAddress ipaddr = resolver.search(host);
  return ip_to_string(ipaddr);
}

void setup() {
  Serial.begin(115200);
  delay(200);
  
  WiFi.begin(WIFI_SSID, WIFI_PWD);
  
  wake_wifi();
  
  
  Serial.println("");
  Serial.println("Connected!");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());
  
  // DNSの名前解決は最初の一回だけ行う
  host_ip = get_ip(TARGET_HOSTNAME);

  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  
  int read_val = analogRead(A0);
  //Serial.print("analog read = ");
  //Serial.println(read_val);

  if(state == STATE_OFF && read_val < ON_THRESH){
    // OFF→ONになったとき、GETリクエストを送る
    digitalWrite(LED_PIN, HIGH);

    wake_wifi();  // WiFiが使えるようになるまで待つ必要あり?未解明
    String get_result = getPageSource(String("http://") + host_ip + String("/on"));
    //Serial.println(get_result);
    
    digitalWrite(LED_PIN, LOW);

    state = STATE_ON;
  }else if(state == STATE_ON && read_val > OFF_THRESH){
    // ON→OFFになったとき、GETリクエストを送る
    digitalWrite(LED_PIN, HIGH);

    wake_wifi();  // WiFiが使えるようになるまで待つ必要あり?未解明

    
    String get_result = getPageSource(String("http://") + host_ip + String("/off"));
    //Serial.println(get_result);

    digitalWrite(LED_PIN, LOW);

    state = STATE_OFF;
  }

  delay(10);
}

赤外線LED側の設計

WiFiリクエストを受け取って、エアコン用の赤外線を出す装置を作ります。必要なものはESP8266と、赤外線LED、電流増幅用のトランジスタと抵抗です。

ソースコード(赤外線LED側)

WiFiリクエストを受け取って何かをするのはWebServer.hを使います。まずは→ライブラリ管理→でESP8266WebServerと検索してライブラリをダウンロードします。

赤外線の送信は「」というライブラリを使いました。こちらはTOSHIBA製や日立製など主要なエアコン製品の赤外線信号を提供してくれているので、好きなコマンドを簡単に送ることが出来ます。

自分が持っているリモコンの信号を覚えさせてそれを再生する方法もありますが、全部のボタンを覚えさせるのが手間なのと、たまに送信ミスすることがあったのでこちらを使うことにしました。

/* IRremoteESP8266: IRsendDemo - demonstrates sending IR codes with IRsend.
 *
 * Version 1.1 January, 2019
 * Based on Ken Shirriff's IrsendDemo Version 0.1 July, 2009,
 * Copyright 2009 Ken Shirriff, http://arcfn.com
 *
 * An IR LED circuit *MUST* be connected to the ESP8266 on a pin
 * as specified by kIrLed below.
 *
 * TL;DR: The IR LED needs to be driven by a transistor for a good result.
 *
 * Suggested circuit:
 *     https://github.com/markszabo/IRremoteESP8266/wiki#ir-sending
 *
 * Common mistakes & tips:
 *   * Don't just connect the IR LED directly to the pin, it won't
 *     have enough current to drive the IR LED effectively.
 *   * Make sure you have the IR LED polarity correct.
 *     See: https://learn.sparkfun.com/tutorials/polarity/diode-and-led-polarity
 *   * Typical digital camera/phones can be used to see if the IR LED is flashed.
 *     Replace the IR LED with a normal LED if you don't have a digital camera
 *     when debugging.
 *   * Avoid using the following pins unless you really know what you are doing:
 *     * Pin 0/D3: Can interfere with the boot/program mode & support circuits.
 *     * Pin 1/TX/TXD0: Any serial transmissions from the ESP8266 will interfere.
 *     * Pin 3/RX/RXD0: Any serial transmissions to the ESP8266 will interfere.
 *   * ESP-01 modules are tricky. We suggest you use a module with more GPIOs
 *     for your first time. e.g. ESP-12 etc.
 */

#ifndef UNIT_TEST
#include <Arduino.h>
#endif
#include <IRremoteESP8266.h>
#include <IRsend.h>
#include <ir_Toshiba.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>

#define WIFI_SSID "Buffalo-G-4310"
#define WIFI_PWD "buf50317"
#define DNS_NAME "ir-sender"

#define HTML_HEADER "<!doctype html>"\
  "<html><head><meta charset=\"UTF-8\"/>"\
  "<meta name=\"viewport\" content=\"width=device-width\"/>"\
  "</head><body>"
#define HTML_FOOTER "</body></html>"

ESP8266WebServer server(80);

const uint16_t kIrLed = 4;  // ESP8266 GPIO pin to use. Recommended: 4 (D2).
IRToshibaAC ac(kIrLed);  // Set the GPIO to be used for sending messages.

void printState() {
  // Display the settings.
  Serial.println("Toshiba A/C remote is in the following state:");
  Serial.printf("  %s\n", ac.toString().c_str());
  // Display the encoded IR sequence.
  unsigned char* ir_code = ac.getRaw();
  Serial.print("IR Code: 0x");
  for (uint8_t i = 0; i < kToshibaACStateLength; i++)
    Serial.printf("%02X", ir_code[i]);
  Serial.println();
}

void handle_root(){
  String str = HTML_HEADER
    "<h1>I'm IR sender!</h1>"
    "<h2>Usage</h2>"
    "<h3>/</h3>"
    "このページです。<br>"
    "<h3>/set?temp=22&fan=0&mode=a</h3>"
    "デフォルトのパラメータを設定します。<br>"
    "<h3>/state</h3>"
    "デフォルトのパラメータを表示します。<br>"
    "<h3>/on?temp=22&fan=0&mode=a または /on</h3>"
    "エアコンONを送信します。クエリをつけるとそれをデフォルトのパラメータに設定して起動します。<br>"
    "クエリをつけないと以前に設定したパラメータを使います。<br>"
    "<h3>/off</h3>"
    "エアコンOFFを送信します。<br>"
    HTML_FOOTER;

  server.send(200, "text/html", str);
}

void handle_set(){
  for (int i = 0; i < server.args(); i++) {

    if(server.argName(i) == "temp"){
      ac.setTemp(server.arg(i).toInt());
    }else if(server.argName(i) == "fan"){
      ac.setFan(server.arg(i).toInt());
    }else if(server.argName(i) == "mode"){
      if(server.arg(i) == "a") ac.setMode(kToshibaAcAuto);
      if(server.arg(i) == "h") ac.setMode(kToshibaAcHeat);
      if(server.arg(i) == "c") ac.setMode(kToshibaAcCool);
      if(server.arg(i) == "d") ac.setMode(kToshibaAcDry);
    }
  
  }
}

void handle_on(){
  if(server.args() > 0)
    handle_set();
  
  ac.on();
  ac.send();

  String str = HTML_HEADER + ac.toString() + HTML_FOOTER;
  server.send(200, "text/html", str);
}

void handle_off(){
  ac.off();
  ac.send();

  String str = HTML_HEADER + ac.toString() + HTML_FOOTER;
  server.send(200, "text/html", str);
}

void setup() {

  ac.begin();
  Serial.begin(115200);
  delay(200);
  
  WiFi.begin(WIFI_SSID, WIFI_PWD);
  
  // Wait until WiFi is connected
  Serial.println("");
  while(WiFi.status() != WL_CONNECTED){
    delay(1000);
    Serial.print(".");
  }
  
  Serial.println("");
  Serial.println("Connected!");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());

  if (MDNS.begin(DNS_NAME)) {
    Serial.println("MDNS responder started");
  }

  // Setup WebServer Handlers
  server.on("/", handle_root);
  server.on("/on", handle_on);
  server.on("/off", handle_off);
  server.on("/set", handle_set);

  server.on("/state", [](){
    String str = HTML_HEADER + ac.toString() + HTML_FOOTER;
    server.send(200, "text/html", str);
  });

  server.begin();
}

void loop() {
  
  server.handleClient();
  MDNS.update();
}

回路図

よくあるLEDを光らせる回路と同じです。僕が買った赤外線LEDは3.3Vでは光が弱すぎてエアコンまで届かなかったので、5VをトランジスタでON/OFFするようにしています。

赤外線LEDをトランジスタでON/OFFするだけ
基盤もなしでそのまま配線しちゃいます

また、このままだと5VがLEDに直接かかっているので、電流が流れすぎてLEDを壊す可能性がありますが、たくさん電流を流したほうが赤外線がよく届くのであえて抵抗を入れていません。

今のところ問題なく動いていますし、LEDが光るのは一瞬なので大丈夫だとは思いますが、最初は抵抗をはさみつつテストする方が良いでしょう。

適当な箱に入れて目立たなくする

置物側を作るときに使った箱のもう半分を利用して適当に作ってみました。木の箱に入れるだけでちゃんとしてる感が出ますね!

LEDやトランジスタにジャンパワイヤを切ってハンダ付け。テープで巻いときゃなんとかなる。
木箱からのぞくLED…

完成!

置物を目立つところに、LEDの箱を目立たないところに置いておきましょう。置物を動かすと…クーラーが付きます♪

以上です。

吹き消しキャンドルを作る

はじめに

前回部屋の明かりをIoT化してWiFiから消したり点けたりできるようになりました。でも消そうと思うたびにスマホを取り出してブラウザで操作しないといけないので、ちょっとスマートじゃないなということで息を吹きかけると消える装置を作ってみました。

手元にあったLEDキャンドルをリメイクしてみたかったっていうのもある。

つくりたいもの

  • 息を検出する
  • 検出したらWiFiを介してHTTPのGETリクエストを送る(またはLEDを消す)

必要なもの

  • ESP8266モジュール
  • または適当なArduino(LEDを消すだけ)
  • コンデンサマイク(※アンプなしのもの。)
  • 抵抗(2kΩ✕1、200Ω✕2)
  • コンデンサ(0.01μF✕1)
  • LED✕1
  • ブレッドボード、ジャンパワイヤー

↓いつもの安くておすすめなやつです

ブレッドボードを組み立てる

必要なものを揃えたらブレッドボードにさしていきましょう。回路図はこんな感じです。

吹き消しキャンドル回路図
ブレッドボード配置図。できるだけ回路図と同じになるように表示しています。

最低限の部品しか使っていませんがこれで動きます。たぶん詳しい方からすればミスが多いと思いますので、ご遠慮無くご指摘ください。簡単にポイントを説明します。

ポイント① マイク部分

この部分はコンデンサマイクの基本的な回路です。データシートにも書いてあります。抵抗R3とコンデンサC1の値はデータシートとかなり違いますが、これしか手元になかったので使っています。一応問題なく動いているのでOKとします。
コンデンサマイクは極性があるのでプラスとマイナスを間違えると動きません。逆に、コンデンサC1は極性があるとだめなのでセラミックコンデンサを使います。

ポイント② バイアス回路

この部分はバイアス回路です。コンデンサマイクからの信号に一定電圧をプラスして信号を見やすくします。

僕が使っているESP8266モジュールはアナログ入力が0~3.3Vなので、その半分くらいの1.65Vが足されるようにしました。

アナログ入力が0~1Vのesp8266 もある(むしろこっちが一般的?)ので、その場合はバイアスが0.5VくらいになるようにR1とR2の値を決めてください。

ポイント③ LED

動作確認用にLEDを付けています。普通はLEDに電流が流れすぎないように数百Ωの抵抗を入れたほうが絶対いいですが、これでも動くのでテストだけなら抵抗無しでやってます。長時間使うとあんま良くないと思います。

【注意!】esp8266に電源を差し込む前に、回路が間違っていないか確認しましょう。特にesp8266の3.3VとGNDがショートしてるとヤバイです。過大な電流が流れてesp8266を壊します。僕はこれで2つ壊しました。テスター等で必ず確認してください。

ESP8266にプログラムを書き込む

Arduino IDEを使ってesp8266にプログラムを書き込みます。コードの全体像はこちらです。

/*
 * 吹き消すと火が消えるライト
 * 息はコンデンサマイクで検知。
 */

#define A1 0.9999  // 定常値を求めるLPFの係数。長期的なセンサ値の平均を求める。[]

// 検出判定のパラメータ。TIME_DURATION[s]間にCOUNT_MAX回しきい値を超えたら反応する。
#define TIME_DURATION 500  // [ms]
#define COUNT_MAX 5  // []
#define V_DIFF 2.0  // 閾値

#define LED_PIN 14


float v = 0;  // センサ値
float v_stable = 0;  // センサ値の定常値
unsigned long last_detected_time = millis();  // 最後にセンサ値が閾値を超えた時間
int detect_count = 0;  // 検出回数

void setup() {
  Serial.begin(115200);

  // 最初のanalogReadは値が信用できないので、何回か読む。
  for(int i=0; i<50; i++){
    v_stable = analogRead(A0);
    delay(10);
  }

  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, HIGH);
}

void loop() {
  v = analogRead(A0);  // センサ値
  v_stable = A1*v_stable + (1-A1)*v;  // センサ値にローパスフィルタをかけたもの。急には変化しない。

  // 閾値を超えたらカウントを増やす
  if( fabs(v - v_stable) > V_DIFF ){
    last_detected_time = millis();
    detect_count++;
  }

  // タイムアウト
  if( millis() - last_detected_time > TIME_DURATION )
    detect_count = 0;

  // カウントが溜まったらLEDを消灯
  if( detect_count > COUNT_MAX ){
    digitalWrite(LED_PIN, LOW);
    delay(1000);
    digitalWrite(LED_PIN, HIGH);
    detect_count = 0;
  }
  
  Serial.println(v);
  delay(10);
}

息の検出アルゴリズム

loop()関数内で息の検出をしています。コンデンサマイクはかなり小さい信号しか出すことが出来ないので、通常はアンプ回路で信号を増幅しますが、息くらいの大きい振動ではアンプ無しでも検出できるくらいに信号が変化します。今回のプログラムではこの信号の変化をトリガーにしてアクションを起こします。

息以外の音でも反応してしまいそうですが、意外とそんなことはありませんでした。

書き込み

僕と同じモジュールを使っている方は以下の設定で大丈夫です。

ESP8266モジュールの書き込み設定

完成!

コンデンサマイクに直接息を吹きかけてみましょう。LEDが消えます。

写真では分かりづらいですが、LEDが消えました!

もし消えなかった方はプログラム中の閾値やMAX_COUNTを減らすなどして調整してみてください。

以上です。

ESP32のタッチセンサの使い方と外れ値処理

はじめに

Arduinoの無線版であるESP32(正式名称はESP-WROOM-32)モジュールにはデフォルトでタッチセンサが10個もついています。ライブラリもあって使うのは非常に簡単です。今回はタッチセンサの使い方と、実際に自作デバイスを作るときのノウハウについて書きます。

使い方

無線チップが乗っていてArduinoとしてプログラミングもできるモジュールESP32を使用します。


ちなみに20円安くwavesという所からも出てますが、ピンヘッダの半田付けが悪くブレッドボードに刺さりにくいというレビューがあったので、こちらのHiletgoの方をおすすめします。ピンヘッダも問題ありませんでした。

ArduinoIDEにライブラリをインストールするのはこちらを参考にしました。
ESP32-DevKitCを使ってみた〜環境構築からタッチセンサによるLチカまで〜

タッチセンサを使うプログラムはめちゃくちゃ簡単で、次のコードだけです。(サンプルコードそのまま)

// ESP32 Touch Test
// Just test touch pin - Touch0 is T0 which is on GPIO 4.

void setup()
{
  Serial.begin(115200);
  delay(1000); // give me time to bring up serial monitor
  Serial.println("ESP32 Touch Test");
}

void loop()
{
  Serial.println(touchRead(T0));  // get value using T0
  delay(1000);
}

何も宣言しなくても、「touchRead(T0)」を書くだけで値の取得ができます。T0ピンに触れていないときは40~70の値、触れてるときは10~30の値になります。

タッチ判定

このタッチセンサを使うシチュエーションとしては、センサに触ったら何かの動作をさせることが多いと思います。タッチの判定条件として一番簡単でよく使われているのは、「センサ値がt以下になった時、処理を実行する」です。

void setup() {
  Serial.begin(115200);
}

void loop() {

  int t0 = touchRead(T0);
  Serial.println(t0);
  
  if(t0 < 40){
    Serial.println("なんらかの処理");
    /*
     * なんらかの処理
     */

  }

  delay(10);

}

しかし、これだとセンサに触れている間はずっとt0<40になってしまい、処理が連続で実行されてしまいます。そこで、一度タッチしたらセンサから指を離すまで待つことが必要です。

void setup() {
  Serial.begin(115200);
}

void loop() {

  int t0 = touchRead(T0);
  Serial.println(t0);
  
  if(t0 < 40){
    Serial.println("なんらかの処理");
    /*
     * なんらかの処理
     */

    //継続して触れている間は何もしない
    while(t0 < 40){
      t0 = touchRead(T0);
      Serial.println(t0);
      delay(10);
    }
  }

  delay(10);

}

たまに外れ値が出る。。。

はじめは上のプログラムで動かしていたのですが、このタッチセンサはたまに触れていないのに0を出すことがあります。

センサ値をプロットしたところ、触れていないのに変な値が出ることがある

この外れ値は連続で出るわけではなく、一瞬出てすぐに普通の値に戻ります。そのような外れ値に対応するための処理として急激な変化の影響を少なくする、ローパスフィルタというものがあります。ローパスフィルタのプログラムは簡単で、前の値を覚えておいて今回の値の方にちょっとずらすことを繰り返します。

サンプルコードはこんな感じです。

int t0_old;

void setup() {
  Serial.begin(115200);
}

void loop() {

  int t0 = touchRead(T8)*0.1 + t0_old*0.9;
  t0_old = t0;
  
  Serial.println(t0);

  
  if(t0 < 40){
    Serial.println("なんらかの処理");
    
    while(t0 < 40){
      t0 = touchRead(T8)*0.1 + t0_old*0.9;
      t0_old = t0;
      Serial.println(t0);
      delay(10);
    }
  }

  delay(10);

}

このときのセンサ値をプロットしたものがこちらです。

ローパスフィルタによって外れ値が除去できました!

ローパスフィルタによって外れ値が除去できました。これでタッチ判定が誤作動することも無くなります。あとは何らかの処理を書けば、簡易スイッチの完成です!

帰宅したら自動で解錠するスマートロックを自作する①~オートロック編~

はじめに

ホテルとかオフィスのドアみたいに閉まると自動で施錠してくれるやつ、あれすごく便利ですよね。家を出るときにわざわざポケットから鍵を出して鍵穴に指して回す動作をしなくていいと思ったよりだいぶ楽です。今回はオートロックを自作して自宅の玄関に取り付けた話です。

作りたいもの

まず、作りたいオートロックに必要な機能を決めます。

  1. ドアが閉まったら自動で施錠
  2. 暗証番号で解錠
  3. ICカードで解錠
  4. スマホを持って近づくだけで解錠

1→4になるほど難易度が高いと思います。この記事では最初の「ドアが閉まったら自動で施錠」をご紹介します。 まだ作ってる途中ですので、随時更新していきます。

必要なもの

  • ArduinoまたはESP8266 400円~2000円くらい
  • サーボモータSG92R  830円くらい
  • 100均の下駄箱ライト  108円

ArduinoまたはESP8266

Arduinoの種類は何でも大丈夫ですが、後々解錠にFelica(ICカード)を使う予定ならシリアル通信で152000bpsが使えるボードが必要です。一番良く使われているArduinoUnoは対応していません。下記の安いArduinoNano互換品とかでいいと思います。また今後スマホを使って解錠するなど、WiFiを使って何かすることを考えるならESP8266をおすすめします。ちなみに僕はESP8266で作ってます。

サーボモータ

サーボモータとは、指定した角度まで回転してくれるモーターです。室内側のサムターンを回すので、それなりにパワーのあるものが必要です。僕の家のサムターンはちょっと固めだったのか、持っていたSG90では回せませんでした。
不安な方はよりトルクの大きいSG92Rをおすすめします。こちらはSG90よりも動きが遅いらしいですが、楽々回してくれてかっこ良いです。

100均の開閉センサ付きライト

100均の下駄箱ライトは写真を撮るの忘れましたが、扉に磁石とセンサーを取り付けて双方が離れるとスイッチON、近づくとスイッチOFFするライトです。今回はこれの磁石とセンサー部分を切り取って使っています。100円で開閉センサが手に入るので便利です。
ここで紹介されているようなやつです。)

サムターン回し装置

まずモーターでサムターンを回す装置を作っていきます。3Dプリンターで部品を作っている人も見かけますが、CAD書いて出力するのも面倒なので割り箸とかで作っていきます。

どんな機構にするか

まずサムターンを回すための機構ですが、こんな感じになるように作ります。

サムターン回し機構。木のブロックがついた部分を左右に90度回すことによって鍵が開く。

木のブロックは余ってる木材の端材とかで全然OKです。それをサーボモーターに付属しているプラスチックのパーツに取り付けます。

木の端材をサーボモータについてるパーツにネジ止めしたところ

こうすることによって、ニュートラルにしておけば鍵を使って開けたり手で開けたりできます。モータで開閉するときは左右に90度回転させて、またニュートラルに戻すことで、手動とモータの動作を両立させることができます。

ドアノブに取り付け

割り箸とマスキングテープを使ってモータをドアノブに固定します。ここはドアノブの形状によって色々やり方があると思うので、ご自宅のドアにあったやり方で固定してください。

割り箸でモータを固定
ドアノブにつける。ここのやり方はお好みで。

開閉センサ

ドアが開いているか閉まっているかを検知するセンサを作ります。100均の下駄箱ライトを使えば簡単に作れます。100均のライトは片方が磁石、もう片方が金属板になっていて、磁石が近づくことで金属板が接触して電気が流れる仕組みになっています。今回はこの部分だけを切ってセンサ代わりに使います。

導線を切ってコネクタをはんだ付けする

2つの箱が近づくと導通、離れると絶縁です。

配線

サーボモータと開閉センサが用意できたら配線してみます。サーボモータは接続を間違えないようにESP8266のGND、Vin、D4(PWMピン)につなぎます。サーボモータから出ている線は茶色がGND、赤が5V、オレンジがPWM信号です。

開閉センサはGND、D0につなぎます。2本ありますがどちらの線をどちらにつないでもOKです。

配線イメージ

プログラム

ESP8266にこちらのプログラムを書き込みます。動作としてはすごく簡単で、開閉センサが「開」から「閉」になったタイミングでサムターンを回すだけです。せっかくWiFiが使えるESP8266を使ったので、WiFi経由で開け締めする機能も付けました!

#include <Servo.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>

// ルータ設定
IPAddress ip(192,168,1,102);
IPAddress gateway(192,168,1,1);
IPAddress subnet(255,255,255,0);

// ESP8266のピン番号
const int servo_pin = 2;
const int door_pin = 12;

Servo myservo;             // サーボオブジェクトを生成
int door_sensor = LOW;
int door_sensor_last = LOW;
ESP8266WebServer server(80);  //サーバーオブジェクト


void door_open(){
  myservo.write(170);
  delay(1000);
  myservo.write(90);
}

void door_close(){
  myservo.write(10);
  delay(1000);
  myservo.write(90);
}

void handle_open(){
  door_open();
  server.send(200, "text/html", "opened");
}

void handle_close(){
  door_close();
  server.send(200, "text/html", "closed");
}

void setup() 
{ 
  
  Serial.begin(115200);
  delay(200);

  //固定IPで運用するときの設定
  WiFi.config(ip, gateway, subnet);
  WiFi.begin(WIFI_SSID, WIFI_PWD);

  // WiFiに接続するまで待つ
  Serial.println("");
  while(WiFi.status() != WL_CONNECTED){
    delay(1000);
    Serial.print(".");
  }
  
  Serial.println("");
  Serial.println("Connected!");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());

  // Webサーバを設定
  server.on("/open", handle_open);
  server.on("/close", handle_close);
  server.begin();

  // 開閉センサの入力を内部プルアップにする
  pinMode(door_pin, INPUT_PULLUP);

  // サーボ変数をピンに割り当て
  myservo.attach(servo_pin);

  door_close();
} 

void loop() 
{ 
  // サーバとして待ち受ける
  server.handleClient();

  // 現在のドアの開閉を検知
  // LOW=閉
  // HIGH=開
  door_sensor = digitalRead(door_pin);

  // 「開」→「閉」になったタイミングでサーボモータを回す
  if(door_sensor == LOW && door_sensor_last == HIGH){
    delay(1000);
    door_close();
  }
  
  door_sensor_last = door_sensor;
  
}

ルータ設定やWiFiパスワードの部分はご自身の環境に合わせて変えてください。

Arduinoを使う人はヘッダーファイルとか適宜変更して、それ以外はほとんどそのままで行けると思います。

完成!

プログラムができたら開閉センサを近づけたり離したりしてみてください。近づけたときにサーボモータが回るはずです。

ESP8266を使っている人はスマホやパソコンのURLのところに192.168.xxx.xxx/closeと入力しても動きます。

確認ができたらドアに取り付けてみます。

※注意 オートロックなので閉め出されないように鍵を持っておくこと!動作テスト中に閉め出されたら恥ずかしいです。

ドアに取り付けてマスキングテープでいい感じにしました

最大の問題はUSBケーブルが玄関まで届くかですね。僕は延長コードを3個経由しています。あと家から出るときに室内側からサムターンを回すのが少し大変です。慣れれば簡単に開けられますがちょっとモータが邪魔なので、今度は家の中からスイッチとかで開けられるようにしておきます。

今回は以上です。次回は帰ってきたときに暗証番号的なもので解錠する方法をご紹介します。

次回→