RobTillaart/I2CKeyPad

Differentiate between press and hold

Closed this issue · 8 comments

Hi,

It would be great if the library would provide a function to see if a button is only pressed for a short period of time or for a longer one.

BR

Hi,
I only noticed this issue today, sorry for the inconvenience.

Will look at your question if it is easy to implement or not.


Thinking out loud

Reading the PCF8574 only gives the state at the moment of reading the chip. In a polling implementation it must at least buffer the last state and optionally keep a time stamp to compare current state with the previous one. If state stays pressed one can interpret it as a long press.

@geeks-r-us

Have given it some thoughts and implementing it is almost trivial, however the existing semantics of lastKey would break.
in pseudoCode you could do almost this

int lastKey = KP.getLastKey();
int newKey = KP.getKey();

bool longPress = (lastKey == newKey);
// process further.

However the internal getLastKey() is not reset when getKey() returns I2C_KEYPAD_NOKEY / I2C_KEYPAD_FAIL.
E.g. if you press 8, then release and press 8 again, lastKey() will keep the value 8 and 'fakes' a long press.

As I do not want to break the getLastKey() semantics I need a way to unambiguous detect the long press.

To be continued (might take a few days).

@geeks-r-us
just to let you know I merged develop branch, so I can create a clean PR for this issue.

@geeks-r-us

Can you test / verify the example below if it meet your needs?
Should work with the latest version of the library.
It will be added as example in a next release if you can confirm it works.

It differentiates between PRESS HOLD RELEASE and NOKEY (+ FAIL) states.
Only thing needed is a state variable and the previous key pressed (or not).

//
//    FILE: I2Ckeypad_long_press.ino
//  AUTHOR: Rob Tillaart
// PURPOSE: demo
//     URL: https://github.com/RobTillaart/I2CKeyPad
//
//  PCF8574
//    pin p0-p3 rows
//    pin p4-p7 columns
//  4x4 or smaller keypad.


#include "Wire.h"
#include "I2CKeyPad.h"

const uint8_t KEYPAD_ADDRESS = 0x38;
I2CKeyPad keyPad(KEYPAD_ADDRESS);

uint32_t lastKeyPressed = 0;
uint32_t interval = 100;  //  milliseconds.
//  keep previous key value to detect long keypresses.
uint8_t  prevKey = I2C_KEYPAD_NOKEY;

enum STATE
{
  FAIL    = 0,
  PRESS   = 1,
  HOLD    = 2,
  RELEASE = 3,
  NOKEY   = 4,
};
STATE state = NOKEY;


void setup()
{
  Serial.begin(115200);
  Serial.println(__FILE__);
  Serial.println("I2C_KEYPAD_LIB_VERSION: ");
  Serial.println(I2C_KEYPAD_LIB_VERSION);
  Serial.println();

  Wire.begin();
  Wire.setClock(400000);
  if (keyPad.begin() == false)
  {
    Serial.println("\nERROR: cannot communicate to keypad.\nPlease reboot.\n");
    while (1);
  }
}


void loop()
{
  uint32_t now = millis();
  char keys[] = "123A456B789C*0#DNF";  //  N = NoKey, F = Fail

  //  read the keypad every interval.
  if (now - lastKeyPressed >= interval)
  {
    lastKeyPressed = now;
    //  read the keypad
    uint8_t key = keyPad.getKey();
    switch (key)
    {
      case I2C_KEYPAD_FAIL: state = NOKEY;
        break;

      case I2C_KEYPAD_NOKEY:
        if (key == prevKey) state = NOKEY;
        else                state = RELEASE;
        break;
        
      default:
        if (key == prevKey) state = HOLD;
        else                state = PRESS;
        break;
    }
    Serial.print("STATE:\t ");
    Serial.print(state);
    Serial.print("\t");
    Serial.println(keys[key]);
    prevKey = key;
  }
}


//  -- END OF FILE --

@RobTillaart

Thx you very much that works great.

Here is an example which allows a more natural input.

#include "Wire.h"
#include "I2CKeyPad.h"

const uint8_t KEYPAD_ADDRESS = 0x20;
I2CKeyPad keyPad(KEYPAD_ADDRESS);

uint32_t keyPressStartTime = 0;
uint32_t shortPressThreshold = 200;  // Threshold for short key press
uint32_t longPressThreshold = 1000;  // Threshold for long key press

uint8_t prevKey = I2C_KEYPAD_NOKEY;

enum STATE
{
  FAIL    = 0,
  PRESS   = 1,
  HOLD    = 2,
  RELEASE = 3,
  NOKEY   = 4,
};
STATE state = NOKEY;

void setup()
{
  Serial.begin(115200);
  Serial.println(__FILE__);
  Serial.println("I2C_KEYPAD_LIB_VERSION: ");
  Serial.println(I2C_KEYPAD_LIB_VERSION);
  Serial.println();

  Wire.begin();
  Wire.setClock(400000);
  if (!keyPad.begin())
  {
    Serial.println("\nERROR: cannot communicate with keypad.\nPlease reboot.\n");
    while (1);
  }
}

void loop()
{
  uint32_t now = millis();
  char keys[] = "123A456B789C*0#DNF";

  uint8_t key = keyPad.getKey();

  switch (key)
  {
    case I2C_KEYPAD_FAIL:
      state = FAIL;
      break;

    case I2C_KEYPAD_NOKEY:
      if (key == prevKey)
      {
        // Key release
        state = RELEASE;
        keyPressStartTime = 0;  // Reset the key press start time
      }
      else
      {
        // No key is pressed
        state = NOKEY;
      }
      break;

    default:
      if (key != prevKey)
      {
        // New key press
        state = PRESS;
        keyPressStartTime = now;  // Record the key press start time
      }
      else
      {
        // Key is being held
        if (now - keyPressStartTime >= longPressThreshold)
        {
          // Long key press
          state = HOLD;
        }
        else if (now - keyPressStartTime >= shortPressThreshold)
        {
          // Short key press
          // You can add a delay here if needed
        }
        else
        {
          // Key is being held but not yet a long press
          // You can add additional logic here if needed
        }
      }
      break;
  }

  Serial.print("STATE:\t ");
  Serial.print(state);
  Serial.print("\t");
  Serial.println(keys[key]);
  prevKey = key;
}

The state machine is not 100% yet as the code has a grey area between the two thresholds.

Thinking out loud:

  • a short keypress is detected when a key is released before the threshold. So when NOKEY and duration < threshold.. This implies a conditional transition state SHORTPRESS between PRESS and RELEASE.
  • a long keypress is detected when a key is hold longer than the threshold. This implies a transition state LONGPRESS between HOLD and a new state LONGHOLD.

@geeks-r-us

Wrote an example that differentiates between SHORT, MEDIUM and LONG presses.
Please have a look and let me know what you think of it.

the state FAIL is removed as it was not used (and it gave a conflict on my ESP32 compiler)

//
//    FILE: I2Ckeypad_long_HOLD.ino
//  AUTHOR: Rob Tillaart
// PURPOSE: demo
//     URL: https://github.com/RobTillaart/I2CKeyPad
//
//  PCF8574
//    pin p0-p3 rows
//    pin p4-p7 columns
//  4x4 or smaller keypad.


#include "Wire.h"
#include "I2CKeyPad.h"

const uint8_t KEYPAD_ADDRESS = 0x38;
I2CKeyPad keyPad(KEYPAD_ADDRESS);

uint32_t lastKeyChecked = 0;
uint32_t startKey = 0;
uint32_t interval = 100;  //  milliseconds.
//  keep previous key value to detect long keypresses.
uint8_t  prevKey = I2C_KEYPAD_NOKEY;


#define  SHORT_THRESHOLD     300
#define  MEDIUM_THRESHOLD    600
#define  LONG_THRESHOLD      1200

enum STATE
{
  PRESS        = 1,
  SHORT_PRESS,
  MEDIUM_PRESS,
  LONG_PRESS,
  RELEASE,
  NOKEY,
};
STATE state = NOKEY;


void setup()
{
  Serial.begin(115200);
  Serial.println(__FILE__);
  Serial.println("I2C_KEYPAD_LIB_VERSION: ");
  Serial.println(I2C_KEYPAD_LIB_VERSION);
  Serial.println();

  Wire.begin();
  Wire.setClock(400000);
  if (keyPad.begin() == false)
  {
    Serial.println("\nERROR: cannot communicate to keypad.\nPlease reboot.\n");
    while (1);
  }
}


void loop()
{
  uint32_t now = millis();
  char keys[] = "123A456B789C*0#DNF";  //  N = NoKey, F = Fail

  //  read the keypad every interval.
  if (now - lastKeyChecked >= interval)
  {
    lastKeyChecked = now;
    //  read the keypad
    uint8_t key = keyPad.getKey();
    switch (key)
    {
      case I2C_KEYPAD_FAIL: state = NOKEY;
        break;

      case I2C_KEYPAD_NOKEY:
        if (key == prevKey)
        {
          state = NOKEY;
        }
        else
        {
          state = RELEASE;
        }
        break;

      default:
        if (key == prevKey)
        {
          if (now - startKey < SHORT_THRESHOLD)
          {
            state = SHORT_PRESS;
          }
          else if (now - startKey < MEDIUM_THRESHOLD)
          {
            state = MEDIUM_PRESS;
          }
          else
          {
            state = LONG_PRESS;
          }
        }
        else
        {
          state = PRESS;
          startKey = now;
        }
        break;
    }
    Serial.print("STATE:\t ");
    Serial.print(state);
    Serial.print("\t");
    Serial.println(keys[key]);
    prevKey = key;
  }
}

//  -- END OF FILE --

Published the example in master and closed the issue.
Feel free to reopen if needed.