HoverPin

HoverPin

Touchless PIN pad featuring ToF-sensors, 1D-gesture detection and audio-visual feedback.

  • 1,088 views
  • 1 comment
  • 0 respects

Components and supplies

Necessary tools and machines

09507 01
Soldering iron (generic)

About this project

The idea was to create a touchless PIN pad.

There are 4 components that comprise the system:

  • Arduino MKR 1010
  • MCP23017 port multiplier
  • Seeedstudio 2.8 Tft Display
  • DFPlayer mp3 / wave player + speaker

Core component is a 4x4 sensor board featuring infrared ToF laser sensors:

Replacing the haptics of a conventional display is challenging. So I thought having audio-visual cues would be a good way to provide user feedback.

Code

hoverpinArduino
Arduino code / sketch file for HoverPin
// quick-fix for "macro "min" passed 3 arguments, but takes 2" error and "expected unqualified-id before '(' token"
#include <Arduino.h>
#undef max
#undef min
#undef abs
// quick-fix end

#include <vector>
#include <queue>
#include <algorithm>

#include <Wire.h>
#include <MCP23017.h> 
#include <VL53L0X.h>
#include <TFTv2.h>
#include <SPI.h>
#include "DFRobotDFPlayerMini.h"

#define MCP_ADDRESS 0x20    // A2/A1/A0 = LOW
#define WAIT_TIME_MS 100
#define SENS_TIMEOUT 500   // 500 ms sensor timeout
#define SENS_PERIOD 25     // 50 ms read period
#define DIST_MAX 40
#define DIST_MIN 5
#define DIST_SMA_WINDOW 10
#define DIST_GESTURE 4
#define DRAW_GRID 60
#define DRAW_OFFSET_BUTTON_X 30
#define DRAW_OFFSET_BUTTON_Y 100
#define DRAW_OFFSET_CAPTION_X 20
#define DRAW_OFFSET_CAPTION_Y 90
#define DRAW_BUTTON_RADIUS 25

//#define PRINT_SENSOR_DATA
#define AUDIO

#ifdef AUDIO
DFRobotDFPlayerMini dfPlayer;
#endif

MCP23017 mcp(MCP_ADDRESS,5); 

VL53L0X* sensors[4][4];
uint16_t sens_raw[4][4];
uint8_t sens_addr[4][4];
// TODO: use optimzed ring buffer
std::deque<uint16_t> sens_hist[4][4];
uint16_t sens_sma[4][4];

struct ButtonEvent{
  bool flag;
  int x;
  int y;
  long timestamp;
};

ButtonEvent cursorCurr = {false, -1, -1, 0};
ButtonEvent cursorPrev = {false, -1, -1, 0};
ButtonEvent press = {false, -1, -1, 0};
ButtonEvent release = {false, -1, -1, 0};

const char* btnText[4][4] = {
  {"1", "2", "3", "C"},
  {"4", "5", "6", "Del"},
  {"7", "8", "9", "OK"},
  {"*", "0", "#", "I"}
};

uint16_t btnTextSize[4][4] = {
  {3,3,3,3},
  {3,3,3,2},
  {3,3,3,2},
  {3,3,3,3}
};

uint16_t btnBackground[4][4] = {
  {BLACK, BLACK, BLACK, RED},
  {BLACK, BLACK, BLACK, YELLOW},
  {BLACK, BLACK, BLACK, GREEN},
  {BLACK, BLACK, BLACK, BLUE},
};

char pin[] = "----";
int pinCurrDigit = 0;

void handlerNum(int x, int y){
  pin[pinCurrDigit] = btnText[x][y][0];
  pinCurrDigit = (++pinCurrDigit) % 4;
}

void handlerNoOp(int x, int y){}

void handlerClear(int x, int y){
  pinCurrDigit = 0;
  strcpy(pin, "----");
}

void handlerBack(int x, int y){
  if(--pinCurrDigit < 0)
    pinCurrDigit = 3;
  pin[pinCurrDigit] = '-';
}

typedef void (*ButtonHandler)(int x, int y);

ButtonHandler btnHandler[4][4] = {
  {handlerNum, handlerNum, handlerNum, handlerClear},
  {handlerNum, handlerNum, handlerNum, handlerBack},
  {handlerNum, handlerNum, handlerNum, handlerClear},
  {handlerNoOp, handlerNum, handlerNoOp, handlerNoOp}
};

const int REDRAW_MULT = 3;
int redraw_count = 0;

// ================================================================

void setup(){ 

  Serial.println("initializing display...");

  TFT_BL_ON;                                          //turn on the background light  
  Tft.TFTinit();                                      //init TFT library

  Tft.drawHorizontalLine(0,60, 240, WHITE);
  Tft.drawString("PIN:", 20, 20, 3, WHITE, TextOrientation::PORTRAIT);
  Tft.drawString(pin, 100, 20, 3, WHITE, TextOrientation::PORTRAIT);
  delay(2000);

  for(int i = 0; i < 4; i++){
      for(int k = 0; k < 4; k++){
          Tft.fillCircle(DRAW_OFFSET_BUTTON_X + k*DRAW_GRID, DRAW_OFFSET_BUTTON_Y + i*DRAW_GRID, DRAW_BUTTON_RADIUS-2, btnBackground[i][k]);
          Tft.drawString(btnText[i][k], DRAW_OFFSET_CAPTION_X + k*DRAW_GRID, DRAW_OFFSET_CAPTION_Y + i*DRAW_GRID, btnTextSize[i][k], WHITE, TextOrientation::PORTRAIT);
          Tft.drawCircle(DRAW_OFFSET_BUTTON_X + k*DRAW_GRID, DRAW_OFFSET_BUTTON_Y + i*DRAW_GRID, DRAW_BUTTON_RADIUS, WHITE);          
      }
  }      

  delay(WAIT_TIME_MS);

#ifdef AUDIO
  Serial.println("initializing audio...");
  Serial1.begin(9600);
  if (!dfPlayer.begin(Serial1)) {  
    Serial.println(("error initializing Serial1 for DFPlayer"));
  }
    
  dfPlayer.volume(20);  //Set volume value. From 0 to 30
  dfPlayer.play(1);  //Play next mp3 every 3 second.

#endif


  Serial.println("initializing sensors...");

  for(int i = 0; i < 4; i++){
    for(int k = 0; k < 4; k++){
      sensors[i][k] = new VL53L0X();
    }    
  }

  Wire.begin();
  mcp.Init();

  Serial.println("setting port modes...");
  mcp.setPortMode(B11111111, MCP_PORT::A);  
  mcp.setPortMode(B11111111, MCP_PORT::B);  
  delay(WAIT_TIME_MS);

  mcp.setPort(B00000000, MCP_PORT::A);
  mcp.setPort(B00000000, MCP_PORT::B);
  delay(WAIT_TIME_MS);

  Serial.println("activating sensors...");
  // activating sensors one by one
  for(int i = 0; i < 4; i++){
   for(int k = 0; k < 4; k++){
     int idx = i*4 + k;
     // mask goes 00000001 -> 00000011 -> 00000111 ...
     uint8_t mask = ((B00000001 << ((idx % 8)+1))) - 1 ;     
     mcp.setPort(mask, idx < 8 ? MCP_PORT::A : MCP_PORT::B);
     delay(WAIT_TIME_MS);
     sensors[i][k]->init(true);
     delay(WAIT_TIME_MS);
     sensors[i][k]->setAddress((uint8_t) (idx + 1));
     sens_addr[i][k] = sensors[i][k]->getAddress();
   }    
 }

  for(int i = 0; i < 4; i++){
    for(int k = 0; k < 4; k++){      
      sensors[i][k]->setTimeout(SENS_TIMEOUT);      
    }    
  }

  for(int i = 0; i < 4; i++){
    for(int k = 0; k < 4; k++){      
      sensors[i][k]->startContinuous(SENS_PERIOD);
    }    
  }

  Serial.print("setup done");  
}


// ================================================================

unsigned long prevTime;
unsigned long currTime;

void loop(){

  // currTime = millis();
  // Serial.println(currTime - prevTime);
  // prevTime = prevTime;
  
  // read sensors
  for(int i = 0; i < 4; i++){
    for(int k = 0; k < 4; k++){
      sens_raw[i][k] = sensors[i][k]->readRangeContinuousMillimeters();
      if(sensors[i][k]->timeoutOccurred()){Serial.print("timeout read");}
    }
  }


  // maintain sensor queues
  for(int i = 0; i < 4; i++){
    for(int k = 0; k < 4; k++){
      // if sensor value within defined valid window, put it in the queue
      if(sens_raw[i][k] > DIST_MIN && sens_raw[i][k] < DIST_MAX)
        sens_hist[i][k].push_back(sens_raw[i][k]);
      // ... make queue run empty without valid values
      else {
        if (sens_hist[i][k].size() > 0)
          sens_hist[i][k].pop_front();
        }
      // limit queue size
      if(sens_hist[i][k].size() > DIST_SMA_WINDOW)
        sens_hist[i][k].pop_front();
    }
  }

  uint16_t min = UINT16_MAX;
  cursorCurr.flag = false;
  cursorCurr.x = -1;
  cursorCurr.y = -1;

  // check if finger / cursor present
  for(int i = 0; i < 4; i++){
   for(int k = 0; k < 4; k++){
      uint32_t res = 0;

      for (auto dist = sens_hist[i][k].cbegin(); dist != sens_hist[i][k].cend(); ++dist){
          res += *dist;
       } // for hist              

      if(sens_hist[i][k].size() > 0){
        res /= sens_hist[i][k].size();
        sens_sma[i][k] = res;
      }
      else{
        sens_sma[i][k] = UINT16_MAX;
      }

      //if(sens_sma[i][k] < min){
      if(sens_raw[i][k] < min && sens_raw[i][k] > DIST_MIN && sens_raw[i][k] < DIST_MAX){
        min = sens_raw[i][k];
        cursorCurr.flag = true;
        cursorCurr.x = i;
        cursorCurr.y = k;
      }
    }
  }

  // if cursor detected, check for button press
  if(cursorCurr.flag){
      int diffOverall = 
          *std::max_element(sens_hist[cursorCurr.x][cursorCurr.y].cbegin(), sens_hist[cursorCurr.x][cursorCurr.y].cend()) -
          *std::min_element(sens_hist[cursorCurr.x][cursorCurr.y].cbegin(), sens_hist[cursorCurr.x][cursorCurr.y].cend());

      diffOverall = std::abs(diffOverall);
      
      bool down = true;
      bool up = true;

      int diffSum = 0;
      for (auto dist = sens_hist[cursorCurr.x][cursorCurr.y].cbegin(); dist != sens_hist[cursorCurr.x][cursorCurr.y].cend(); ++dist){

        if(dist != sens_hist[cursorCurr.x][cursorCurr.y].cbegin()){
          auto prev = std::prev(dist);
          int diff = *dist - *prev;
          diffSum += diff;
          //Serial.print(diff);
          //Serial.print("\t");
          if(diff < 0){
            up = false;
          }
          else if (diff > 0){
            down = false;       
          }
          else{
            up = down = false;
          }
        }
      }
      //Serial.println("");
  
    if(diffSum < -DIST_GESTURE){ 
    //if(down && diffOverall > DIST_GESTURE){ 
      Serial.println("down");
      press.flag = true;
      press.timestamp = millis();
      press.x = cursorCurr.x;
      press.y = cursorCurr.y;
    }

    if(diffSum > DIST_GESTURE){
    //if(up && diffOverall > DIST_GESTURE){
      Serial.println("up");
      release.timestamp = millis();
      release.flag = true;
      release.timestamp = millis();
      release.x = cursorCurr.x;
      release.y = cursorCurr.y;
    }

    if(press.flag && release.flag && release.timestamp - press.timestamp < 1000){
      Serial.print("clicked ");
      Serial.println(btnText[cursorCurr.x][cursorCurr.y]);
      // call corresponding button handler
      btnHandler[cursorCurr.x][cursorCurr.y](cursorCurr.x, cursorCurr.y);
      press.flag = false;
      release.flag = false;
      dfPlayer.stop();
      dfPlayer.play(2);
    }

    if(cursorPrev.x != cursorCurr.x || cursorPrev.y != cursorCurr.y){
      
      //Serial.println("cursorChanged"); 
#ifdef AUDIO
      dfPlayer.stop();
      dfPlayer.play(1);
#endif
    }
  }
  cursorPrev = cursorCurr;

//char data[512];
// Serial.println(cursor);
//   for(int i = 0; i < 4; i++){
//    for(int k = 0; k < 4; k++){
//      sprintf(data, "|%5d", sens_addr[i][k]-1);
//      Serial.print(data);
//      Serial.print("\t");
//      sprintf(data, "%5d", sens_raw[i][k]);
//      Serial.print(data);
//      Serial.print("\t");     
//      //sprintf(data, "%5d", sens_hist[i][k].size());
//      sprintf(data, "%5d", sens_sma[i][k]);
//      Serial.print(data);
//      Serial.print("\t");
//    }
//    Serial.println("");
//  }
//  Serial.println("=========================================");

  if(++redraw_count >= REDRAW_MULT){
    redraw_count = 0;

    for(int i = 0; i < 4; i++){
        for(int k = 0; k < 4; k++){
            //Tft.drawString(pin, 100, 20, 3, WHITE, TextOrientation::PORTRAIT);
            Tft.drawCircle(DRAW_OFFSET_BUTTON_X + i * DRAW_GRID, DRAW_OFFSET_BUTTON_Y + k * DRAW_GRID, DRAW_BUTTON_RADIUS, (k==cursorCurr.x && i==cursorCurr.y) ? (RED|BLUE) : WHITE);
        }
    }

    Tft.fillRectangle(100, 20, 100, 25, BLACK);
    Tft.drawString(pin, 100, 20, 3, WHITE, TextOrientation::PORTRAIT);
  }

}

Comments

Similar projects you might like

Open Source Pulse Oximeter for COVID-19

Project tutorial by Arduino “having11” Guy

  • 82,110 views
  • 36 comments
  • 141 respects

Low cost vitals monitoring wearable for frontline workers

Project tutorial by Vishwas Navada

  • 8,460 views
  • 2 comments
  • 35 respects

Touchless Washing Hands Timer

by rjconcepcion

  • 6,801 views
  • 1 comment
  • 19 respects

Coronavirus Live Updator

Project tutorial by Sai Chakradhar

  • 13,125 views
  • 18 comments
  • 47 respects

Touchless faucet with door control system for COVID-19

Project tutorial by Rucksikaa Raajkumar

  • 12,680 views
  • 15 comments
  • 38 respects
Add projectSign up / Login