Fingerprint Safe

by adimiller in Circuits > Arduino

5124 Views, 80 Favorites, 0 Comments

Fingerprint Safe

Fingerprint Safe

This project is based on a previous one I published, where I took an old safe and replaced its control board with an ESP8266 D1 Mini to make it into a modern OTP safe.

Time-Based One-Time Password (TOTP) Smart Safe : 7 Steps (with Pictures) - Instructables

This time, I'm adding a Fingerprint sensor to allow for a second method to unlock the safe, including a way to enroll new fingerprints.

The plan is to have the fingerprint sensor work in parallel to the keypad, meaning that one can open the safe using the keypad's OTP or using the fingerprint.

This gets interesting when you think about how to enroll new fingerprints. Enrolling means "teaching" the safe a new fingerprint which is authorized to open the safe. The fingerprint sensor can store up to 127 signatures, and then tries to match the fingerprint it is presented with, with any of the enrolled ones. To enable enrollment, one must first unlock the safe (either using a previously enrolled fingerprint or using the OTP).

Once unlocked, there is a 10 second window during which the fingerprint's LED shows a purple breathing pattern. If during these 10 seconds the user will type (using the keypad) a number between 1-127 followed by A or B, then the controller enters the enrollment process, and will store the enrolled fingerprint in slow 1-127 according to the input. To enroll, the user needs to place the finger on the fingerprint sensor twice.

Supplies

PXL_20220113_205115904.PORTRAIT.jpg

The fingerprint sensor I used is an R503, which is a capacitive fingerprint sensor, as opposed to optical ones.

My intention was to replace the safe's key cylinder with this fingerprint sensor, but the inner thread diameter of the sensor is 23.5 mm, which is 5.5 mm larger than original key cylinder. I ordered it anyway, assuming that I can find a way to make it fit. Luckily, it turns out that the outer diameter (25 mm) is an exact fit to the safe's plastic cover, so the sensor sits very firmly in place without needing to fit it all the way into the door.

Replacing the Cylinder With the Fingerprint Sensor

PXL_20220113_205132800.PORTRAIT.jpg
PXL_20220113_205019650.jpg
PXL_20220113_205214737.PORTRAIT.jpg
PXL_20220113_205410894.PORTRAIT.jpg
PXL_20220113_205602994.jpg
PXL_20220113_205626363.jpg

To disassemble the cylinder, you simply need to unscrew the back side of the cylinder (from the inner side of the door). This removes the mechanism that allows the key to release the lock. Then there is another washer that needs to be unscrewed, and the cylinder can be removed. I'll need to remove the front panel for easy access.

The fingerprint sensor doesn't fit into the door like the cylinder (23.5mm vs. 18mm), but there was enough room in the panel since the fingerprint sensor needs to be further out than the lock. Once secured, the wires can neatly go inside threaded through the original cylinder hole in the door.

Hardware Connection

PXL_20220113_210818875.jpg
PXL_20220113_211327757.jpg
Diagram.png

Connecting the R503 sensor is pretty straightforward and requires just to wires for communication (TX/RX) to be connected to any data GPIOs on the controller (not to be confused with the controller's TX/RX pins). In addition, it requires 3v and GND.

This actually proved a little challenging in the context of my previous project since I didn't have the right pins available and using D8 and D0 didn't work due to their impact on device boot. I finally had to do some changes to how the Buzzer and the relay connect and found pins D6 and D7 as a good fit for the sensor's TX and RX respectively.

Arduino  |  Component  
---------+-------------
D0       |  Keypad-Col1     
D5       |  Buzzer-Red
D6       |  R503-TX
D7       |  R503-RX
D8       |  Keypad-Col2
TX       |  Keypad-Row1
RX       |  Keypad-Row2
D1       |  Relay Signal
D2       |  Keypad-Row3
D3       |  Keypad-Row4
D4       |  Keypad-Col3
3V       |  R503-VCC
GND      |  R503-GND, Relay-GND, Buzzer-Black
5V       |  Relay-VCC

Verifying Fingerprint

Verifying the fingerprint is relatively simple. The Adafruit_Fingerprint library includes a working sample. This is a simplified version of the code I used. See below for the full code.

#include <Adafruit_Fingerprint.h>

#define buzzer D5

// ## Fingerprint reader
SoftwareSerial mySerial(D6, D7);
Adafruit_Fingerprint finger = Adafruit_Fingerprint(&mySerial);
unsigned long lastTry = 0;

void setup() {
  pinMode(buzzer, OUTPUT);

  finger.begin(57600);

  if (!finger.verifyPassword()) {
    Serial.println("Fingerprint reader not found!");
    for (int i = 0; i < 3; i++) {
      tone(buzzer, 4500, 500);
      delay(1000);
    }
  }

  finger.getTemplateCount();
  if (finger.templateCount == 0) {
    Serial.println("Fingerprint reader doesn't have any signatures!");
    for (int i = 0; i < 2; i++) {
      tone(buzzer, 4500, 500);
      delay(1000);
    }
  }
}

void loop() {
  // Check fingerprint
  if (millis() >= lastTry) {
    finger.LEDcontrol(FINGERPRINT_LED_OFF, 0, FINGERPRINT_LED_BLUE);
    int fingerId = getFingerprintID();
    if (fingerId > 0) {
      openSafe();
    } else if (fingerId == -1) {
      wrongPin();
    }
  }
}

void openSafe() {
  finger.LEDcontrol(FINGERPRINT_LED_GRADUAL_ON, 200, FINGERPRINT_LED_BLUE);
  digitalWrite(relayLockPin, LOW); 
  delay(100);
  tone(buzzer, 3000, 500;
  lastTry = millis() + 10000;
}

void wrongPin() {
  finger.LEDcontrol(FINGERPRINT_LED_FLASHING, 25, FINGERPRINT_LED_RED, 3);
  delay(100);
  for (int i = 0; i < 2; i++) {
    tone(buzzer, 4500, 100/beepFactor);
    delay(200);
  }
  tone(buzzer, 4500, 300/beepFactor);
  delay(400);
}

int getFingerprintID() {
  uint8_t p = finger.getImage();
  if (p == FINGERPRINT_NOFINGER || finger.image2Tz() != FINGERPRINT_OK) { 
    return 0;
  }

  p = finger.fingerSearch();
  if (p == FINGERPRINT_OK) {
    return finger.fingerID;
  } else {
    lastTry = millis() + 1000;
  }

  return -1;
}


We specify the GPIO pins used for TX/RX and initialize the fingerprint sensor.

SoftwareSerial mySerial(D6, D7);
Adafruit_Fingerprint finger = Adafruit_Fingerprint(&mySerial);


During setup(), we first verify that the fingerprint sensor is available. If not, this is the place where we want to either quit or just set a flag to make sure the rest of the functionality is disabled.

In addition, we can check the number of signatures that are stored in the sensor (aka templates), and give some audible feedback in case there aren't any.

void setup() {
  finger.begin(57600);

  if (!finger.verifyPassword()) {
    Serial.println("Fingerprint reader not found!");
  }

  finger.getTemplateCount();
  if (finger.templateCount == 0) {
    Serial.println("Fingerprint reader doesn't have any signatures!");
    for (int i = 0; i < 2; i++) {
      tone(buzzer, 4500, 500);
      delay(1000);
    }
  }
}


Inside the loop() we continuously scan the fingerprint. getFingerprintID() returns 0 in case there isn't any finger on the sensor, or in case the scan fails. If there was a finger on the sensor, it will return the ID (1-127) of the matched finger or -1 otherwise.

void loop() {
  int fingerId = getFingerprintID();
  if (fingerId > 0) {
    openSafe();
  } else if (fingerId == -1) {
    wrongPin();
  }
}

Enroll a New Fingerprint

This is the more complex part of the code. This is how we enroll new fingerprints. I removed parts of the code for brevity. See the full project for a version that complies.

void loop() {
  // Fingerprint enroll
  char customKey = customKeypad.getKey();
  if (customKey) { 
    if (customKey != 'A' && customKey != 'B' && codeIndex <= 3) {
      code[codeIndex] = customKey;
      codeIndex = codeIndex + 1;
      Serial.print("Fingerprint Id: ");
      Serial.println(code);
    } else {
      uint16_t slotId = atoi(code);
      codeIndex = 0;
      memset(code, 0, 8);
      if (slotId >= 1 && slotId <= 127) {
        Serial.print("Fingerprint Id to enrol: "); Serial.println(slotId);
        if (getFingerprintEnroll(slotId)) {
          // Success
        } else {
          // Error
        }
      } 
    }
  }
}

bool getFingerprintEnroll(uint8_t id) {
  if (sampleFinger(1)) {
    Serial.print("Remove finger.");
    while (finger.getImage() != FINGERPRINT_NOFINGER) {
      Serial.print(".");
      delay(10);
    }
    Serial.println();
    delay(500);

    if (sampleFinger(2)) {
      Serial.println("Creating model");
      int p = finger.createModel();
      if (p == FINGERPRINT_OK) {
        Serial.println("Prints matched!");
        p = finger.storeModel(id);
        if (p == FINGERPRINT_OK) {
          Serial.println("Stored!");
          return true;
        } else {
          Serial.println("Error storing fingerprint model");
        }    
      } else {
        Serial.println("Fingerprints did not match or other error");
      }
    }
  }
  return false;
}

bool sampleFinger(uint8_t slot) {
  int p = -1;
  while (p != FINGERPRINT_OK && p != FINGERPRINT_PACKETRECIEVEERR) {
    p = finger.getImage();
    switch (p) {
      case FINGERPRINT_OK:
        Serial.println("Image taken");
        break;
      case FINGERPRINT_NOFINGER:
        Serial.print(".");
        break;
      case FINGERPRINT_PACKETRECIEVEERR:
        Serial.println("Communication error");
        break;
      case FINGERPRINT_IMAGEFAIL:
        Serial.println("Imaging error");
        return false;
      default:
        Serial.println("Unknown error");
        return false;
    }
  }

  p = finger.image2Tz(slot);
  switch (p) {
    case FINGERPRINT_OK:
      Serial.println("Image converted");
      break;
    case FINGERPRINT_IMAGEMESS:
      Serial.println("Image too messy");
      break;
    case FINGERPRINT_PACKETRECIEVEERR:
      Serial.println("Communication error");
      break;
    case FINGERPRINT_FEATUREFAIL:
      Serial.println("Could not find fingerprint features");
      break;
    case FINGERPRINT_INVALIDIMAGE:
      Serial.println("Could not find fingerprint features");
      break;
    default:
      Serial.println("Unknown error");
      break;
  }
  return (p == FINGERPRINT_OK);
}


Inside the loop(), we check that the number entered (prior to typing A or B) is a valid slot number for a fingerprint (1-127). If so, we call the getFingerprintEnroll() function with the value of the provided slot.

The enrollment process takes a first sample of the fingerprint, and then waits until the fingerprint sensor reports FINGERPRINT_NOFINGER to indicate that the finger was removed. Then takes a second sample. There are some tests made for each sample, and then the two samples are tested to make sure they match.

int p = finger.createModel();
if (p == FINGERPRINT_OK) {
  Serial.println("Prints matched!");
}


Finally, if all went well, the newly created model is stored.

p = finger.storeModel(id);
if (p == FINGERPRINT_OK) {
  Serial.println("Stored!");
}

Putting It All Together

The full version of the code is available on Github adi-miller/totp-safe. It includes support for OTP, operating the lock and more.

I didn't test it using any other fingerprint sensors but in theory it should work. Happy to hear about your experience in the comments.

Repeating the disclaimer from the original Time-Based One-Time Password (TOTP) Smart Safe - Instructables project:

Needless to say this Safe isn't very safe. There are many potential failure points here, besides the actual safe. If the D1 Mini dies, you will not be able to operate this at all. Bottom line - be careful.


Thanks for reading. Happy to hear what you think and if you are trying to build this or something similar. Please leave comments below or follow me on Twitter @adi_miller.