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

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

作りたいもの

前回 のつづきです。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に別ファイルとして書いてます。

以上です。

(追記予定)

コメントを残す

メールアドレスが公開されることはありません。