Reading lengt of button-press

Hi all!
i want to use 2 functions for a momentary button, turn on/off my bicycle headlight with a long click, and change headlight mode with short clicks.
The goal is to store the mode and when it turns on, directly go to selected mode, instead of having to click thru lowest brightness -> -> -> highest brightness.

So the hardware for this part of my project has 2 outputs and one input.
Outputs are MCP23007 pin 2 (headlight power) and mcp pin 3 (mode select)
input is a simple momentary switch connected to ground.

Headlight power will be a transistor that acts as a switch between headlight and battery while
mode select pin is controlling a transistor thats soldered in place of the mode select button of said headlight.

The headlight button cycles from off to low output and then 1 or 2 medium steps, then to high and then back to off.
To simplify things i figure i cut power for about 50ms to reset the headlight mode, then trigger the button transistor with the "mode" number of flashes.

I have barely a clue what im doing when coding, so please dont spank me.. :slight_smile:
and this is the part im fiddling with, so it does not have all the defining bits and bobs..

But as im reading this incomprehensible jungle of words, the thought strikes me, is there a easier way to do this?

..my code is not quite working just yet, i do get incremental mode feedback (that keeps repeating while the button is pressed) from serial monitor, but there´s not much from the troubleshooting LEDs connected to the output pins.
Also, i seem to be missing the read button_up so to speak, to get correct button down-time.
After 14+ hours of non-stop coding its definitely time for some shuteye and hope for some wize replies in the morning..

So if there is a easier way to do this, then i wont have to try to get this house of cards working.. :slight_smile:

//headlight high beam code, WITH mode select memory
 HighBeamVal = digitalRead(ModeSwitch);		// read modeswitch pin and store in highbeamval
 if ((HighBeamVal == LOW) && (previousHighBeamVal == HIGH)){ // if button is pressed but was not, set timestamp and previousval
    previousHighBeamVal = HighBeamVal; //sets the current status as the old status in preparation for the next check
    HighBeamMillis = currentMillis; //sets a time stamp at the current time
  }
 	if ((HighBeamVal == HIGH) && (previousHighBeamVal == LOW)){ 
    HeadLightConfirm = 0;
 	}
    if	(currentMillis - HighBeamMillis < interval){ // if the button is released when it was pressed and if button press time is less than interval value, turn ON/OFF High Beam
 	 	  if (HighBeam == 0){HighBeam = 1;} // turn on if off, else turn off.
 	 	  else {HighBeam = 0;}
       if (SerialTroubleshoot == 1){     // If Serial troubleshoot is active, report status.
        Serial.print("High/Low beam mode switch ");
        Serial.print(HighBeam);
        Serial.println(" ");}
 	  }
 	
 	 	if ((HighBeamVal == LOW) && (currentMillis - HighBeamMillis > interval)){ // if button press time is more than interval value, select next High Beam mode.
      if (HeadLightConfirm == 0){
        HeadLightConfirm = 1;
        HeadLightMode ++;
      if ((SerialTroubleshoot == 1) && (HeadLightConfirm == 0)){     // If Serial troubleshoot is active, report status.
        Serial.print("HeadLightMode increased, now mode ");
        Serial.print(HeadLightMode);
        Serial.println(" Selected");}
      
      if (HeadLightMode >= 4){HeadLightMode = 0;}
      }
 	 	  if ((HighBeam == 1) && (LightStatus == 1)){ //  and if highbeam active
 	 	  	mcp.digitalWrite(1, HIGH);
 	 	 	  delay(wait);
         mcp.digitalWrite(1, LOW);
 			  for (int i=0; i <= HeadLightMode; i++){
      			mcp.digitalWrite(2, HIGH);
     			  delay(wait);
      			mcp.digitalWrite(2, LOW);
      			delay(wait);}
      	}
 	 	}
 	 	
      if (HighBeam == 0){mcp.digitalWrite(1, LOW);}

So it is working? Then don't change anything.

This kind of user interface code always gets messy. There is no way to make it neat. So long as you avoid magic numbers and copy-paste code, it should come out OK.

From your explanation it looks like if the button is not pressed you do nothing, if it is pressed you mark the time it was pressed and if it is released you determine if it was a short or long press and act accordingly.

I tried to simplify the code. If you find this easier to read try it out to see if it prints what you would expect in the serial monitor.

If you want to store the last mode from when the light was last used you'll have to write the mode to EEPROM every time it changes and read it in setup().

const int ModeSwitch = 3;   
bool buttonDown;             // true => button down
long buttonDownStart;        // time button was pressed
int mode=0;                  // current mode
bool lightOn = false;        // true => light on
const int numModes = 4;      // # of modes
const long interval = 1000;  // long press interval
const int deBounce = 50;     // button debounce

void setup()
{

  // initial button state
  buttonDown = (digitalRead(ModeSwitch) == HIGH);

  // mark time button was pressed
  if( buttonDown) {
    buttonDownStart = millis();
    delay(deBounce);  // debounce

  } // if

}

void loop()
{
  // true if button down
  bool newButtondown = (digitalRead(ModeSwitch) == HIGH);

  // has button state changed?
  if( newButtondown != buttonDown )
  {

    // button just pressed?
    if( !buttonDown )
    {

      buttonDownStart = millis();  // record start
      buttonDown = true;           // set flag

      delay(deBounce);  // debounce
    } 
    else {

      // button released

      // long press?
      if( (millis() - buttonDownStart) > interval )
      {
        // turn on / off
        lightOn = !lightOn;

        Serial.println("long press");
        Serial.println(lightOn ? "light on" : "light off");

      } 
      else {

        // short press
        Serial.println("short press");
        Serial.print("mode:  ");
        Serial.println(mode);

        ++mode %= numModes;  // increment mode

      } // else

      buttonDown = false;  // clear flag

    } // else

  } // if
}

Im not saying this is the way to do it but its a code that I wrote for when I needed to use 1 button to do 2 different tasks. Like morgan said this type of code is messy and difficult to follow

int buttonPin = 2;
unsigned long debounceTimeStamp = 0;
unsigned long buttonTimestamp = 0;
byte buttonState = 0;
byte prevbuttonState = 3;
unsigned long duration;
byte flag = 0;
unsigned long A = 0;
unsigned long B = 0;
byte prevbutton = 0;
byte changeDetected = 0;

void setup() {
  Serial.begin(9600);
  pinMode(buttonPin, INPUT_PULLUP);
}

void loop() {

  byte button = digitalRead(2);
  if (button != prevbutton) {
    changeDetected = 1;
  }
  prevbutton = button;

  if (button == LOW) {
    if (millis() - debounceTimeStamp >= 25) {
      A = millis();
      flag = 1;
    }
  }

  if (button == HIGH) {
    B = millis();
    debounceTimeStamp = millis();
  }
  if (changeDetected == 1) {
    if (millis() - buttonTimestamp >= 1000) {
      if ((button == HIGH) && ( flag == 0)) {
        buttonState = 0; //off
      }
    else  if ((B - A > 700) && (flag == 1)) {
        buttonState = 2;
        flag = 0;
      }
     else if (flag == 1) {
        buttonState = 1;
      }
      flag = 0;
      buttonTimestamp = millis();
    }
  }


  if (buttonState != prevbuttonState) {
    switch (buttonState) {
      case 0:
        Serial.println("button is not pressed");
        break;
      case 1:
        Serial.println("button has been pressed");
        break;
      case 2:
        Serial.println("button has been held");
        break;
    }
  }
  prevbuttonState = buttonState;
}

I like to break things up into classes. I'd make an object for the button, and one one for the headlight, or even one for headlight power and one for headlight brighness. The button object listens to the input pin and detects short and long clicks. The headlight objects track selected power level and on/off.

The point of classes is to encapsulate bits. Instead of a swag of variables floating around at the top level, each object has its own variables that it uses, and that the other classes don't 'see'.

class Headlight {
  const int powerPin;
  const int modeSelectPin;

  boolean powerOn = false;
  int highBeam = 0;
  
public:
  Headlight(int powerPin, int modeSelectPin) :
    powerPin(powerPin),
    modeSelectPin(modeSelectPin) {
  }

  void setup() {
    pinMode(powerPin, OUTPUT);
    pinMode(modeSelectPin, OUTPUT);
    digitalWrite(powerPin, LOW);
    digitalWrite(modeSelectPin, LOW);
  } 

  void loop() {
    // no loop code needed 
  }

  void powerClick() {
    // code here to turn the power on and off in response to a click
    // if we are turning the light on, then send clicks to modeSelectPin accoding to highBeam
  }

  void highBeamClick() {
    // code here to adjust the brighness.
    // if the light is not on, ignore the click. Bleh.
    // if the light is on, adjust the value in highBeam and send a click to modeSelectPin
  }


} headlight(1, 2);


class Button {
   const int pin;
   int prevState;
   unsigned long downtimeMs;
public :
   Button(int pin) : pin(pin) {}

   void setup() {
     pinMode(pin, INPUT_PULLUP);
     prevState = digitalRead(pin);
   }

   void loop() {
     int state = digitalRead(pin);
     if(prevState==LOW && state == HIGH) {
       if(millis() - downtimeMs < 25) {
         // debounce - do nothing
       }
       else if(millis() - downtimeMs < 1000) {
         headlight.highBeamClick();
       }
       else {
         headlight.powerClick();
       }
     }
     else if(prevState == HIGH && state == LOW) {
       downtimeMs = millis();
     } 

     prevState = state;
   }
} button(0);

void setup()  {
  button.setup();
  headlight.setup();
}

void loop() {
  button.loop();
  headlight.loop();
}

PaulMurrayCbr:
I like to break things up into classes.

Thanks for those examples. I plan to study it later.

Isn't this the kind of code usually hidden in a library? If so, do you have a favorite way of writing library type code?

I notice I'm not able to open .cpp or .h files from within the Arduino IDE. In order to open the files I end up using AVR Studio which seems very bloated to me (and really slows down this old PC).

Is there an easier way to edit/create library files?

Sorry for going off topic. Hopefully this information pertains to the original question.

Why a library when in this case you might simply place the code in .h and/or .cpp, in the same directory as your .ino file, that way they will show up as tabs within the IDE allowing quicker access during development.

Is there an easier way to edit/create library files?

On Windows I use Notepad++

Thank you all very much for your input!
Im reading your example codes, comparing with my own previous and current code.
Much of it is greek to me, but im slowly getting the hang of it here..
Classes are very interesting but im finding it hard to understand how its used and what its different components are meant doing. But now i have a name to google, so i will get it, eventually! :slight_smile:

In any way, i found a library called "ClickButton" that does exactly one thing, register button press length! :slight_smile:
It can give you the number of presses, both for short clicks and long, meaning you´ll get over 6 separate outputs from a single button.. :slight_smile:

My in-sketch code is now as is in the code snippet below.
I havent included the setup part nor the library itself, but the in-sketch code got ALOT more understandable to me!

The below code is tested and works brilliantly, alltho i do have some issues that i havent fixed yet.
when i turned ON the headlight with longclick, it turned on, selected mode, then turned itself off.
Then it got stuck in some sort of off-mode, that wouldnt turn back on..
The strange thing is, the exact same code works fine, if changed to a short click.
Simple fix, just delete the long click on and turn it on with a short click.. :slight_smile:
The last bug is a interaction with the rest of the light system, probably a misread checkflag, but that one i can solve on my own with a rested head. :slight_smile:

So if the front light is off, a short click will turn it on, but if light is on, a short click will select next mode.
A long click will turn front light off.

// ClicksButton library part (for mode switch)
  
  if ((button1.clicks == 1) && (HighBeam == 1) && (ButtonChange == 0)){  // if short-click (positive) AND headlight ON AND first time reading the change.
      ButtonChange = 1;
      ButtonChangeMillis = currentMillis;
      HeadLightMode ++;           //  stepup mode
      if (SerialTroubleshoot == 1){Serial.print("Mode change, now: ");Serial.println(HeadLightMode);}
      if (HeadLightMode > numModes){HeadLightMode = 0;}
      mcp.digitalWrite(2, HIGH);  //  button output high (button pressed)
      delay(wait);
      mcp.digitalWrite(2, LOW);   //  button output low (button depressed)
      delay(wait);}

   if ((button1.clicks == 1) && (HighBeam == 0) && (ButtonChange == 0)){
    ButtonChange = 1;  
    ButtonChangeMillis = currentMillis;
    HighBeam = 1;// turn on
    mcp.digitalWrite(1, HIGH);   
    for (int i=0; i <= HeadLightMode; i++){ // transmit headlight mode by "blinking" the button transistor.
          delay(wait);
          mcp.digitalWrite(2, HIGH);
          delay(wait);
          mcp.digitalWrite(2, LOW);}
          if (SerialTroubleshoot == 1){Serial.print("HighBeam ON, Mode; ");Serial.println(HeadLightMode);}
   }

    if ((button1.clicks == -1) && (HighBeam == 1) && (ButtonChange == 0) && (currentMillis - ButtonChangeMillis > interval)){ // if long-click (negative) AND headlight ON AND first time reading the change.
      ButtonChange = 1;
      ButtonChangeMillis = currentMillis;
      HighBeam = 0;// turn off
      if (SerialTroubleshoot == 1){Serial.println("HighBeam OFF");}
      mcp.digitalWrite(1, LOW);
        
      } // end if button1 longclick
    
    if (currentMillis - ButtonChangeMillis > interval / 2){ButtonChange = 0;}