HELP with adapting a countdown timer to current project

Hello everyone hope you all are having a great summer ....

Brief intro

I found a project to be 3d printed that came with it's own arduino software and aldo it works there are a few things i would like to chage.

For example:

When a mode is selected, it gives you the option to tell it for how long it will work ( using a rotary encoder) and the countdown starts and it all works, howerver.... it does not show you the actual countdon, it only shows "minutes left" to the end... instaid of 02:00>01:59->01:58 ... etc, it only shows 02:00 left (after 60sec) -> 01:00 left.... etc

i am no programer and before asking for help i have tryed all demos i could find and they all work... but i lack the skill to merge/adapt any of them to my proj.

I would apreciate any help from you guys.

Thank you in advanced
Regars
Xmodpt

// Open clean and cure project
// Made by Jeremy Vallet
// Before upload this sketch, make sure to have correctly set tension on your DCDC converter. It should output 5V
// Also make sure to have correctly set the tension of your driver, my driver is a A4988 with res of 0.1 ohm, I set my Vref to 0.5V. If your driver have a different rsense (Rcs) value, please read the documentation at https://www.pololu.com/product/1182
// Imax = Vref / (8*Rcs) = 0,5/0.8 = 625mA in my case
// So with a Rcs of 0.05 you may set your Vref too 250mV
// And with a Rcs of 0.068 you may set your Vref too 340mV

// If this tension is set to high, your stepper can overheat, your driver too and you may damage something.

// As said in the guide, you can also tweak your current limit on the DCDC 5V converter (one potentiometer is for voltage, and the other for current) to make sure your LED won't overheat (the classic 10W 5V pannel seems to run relatively cold but I prefered to set a current limitation) 

// If you encounter issue with speed rotation, you can tweak values MAX_STEPPERSPEED_WASH and MAX_STEPPERSPEED_CURE with the sketch file nammed "StepperCalibration.ino".

// Visit my Cult3D for more projects ! https://cults3d.com/fr/utilisateurs/jeremyV
// or Thingiverse : https://www.thingiverse.com/jvjv/designs
//////////////////////////////////////////////////////////////////////////////////////
//
//
//  PROGRAMING Updated by Xmodpt
//
//  v.1.0.1 - Added Buzzer for sound alarm when task finishes
//            Corrected the bug with "CURE" timer

#include <AccelStepper.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

// UV LEDs pannel
#define PIN_UVLEDS 4

// rotary encoder
#define CLK 6 //CLK //s1
#define DT 5 //DT //s2
#define RE_KEY 2

// stepper and driver pins
#define PIN_DIR 7
#define PIN_STEP 8
#define PIN_RESET 10
#define PIN_SLEEP 8
#define PIN_ENABLE 11
#define MS1 13
#define MS2 12
#define MS3 3

// speed and acceleration of stepper
#define MAX_STEPPERSPEED_WASH 713 //FULL STEP - value is in step/s - you can this value with the SpeedCalibration.ino file
#define MAX_STEPPERSPEED_CURE 106 // Sixteenth step - 106 is equal to 30sec for 360° of rotation (my NEMA 17 is 200 steps per revolution, microstepping by /16 so 200*16 = 3200, 30s (cause I want 30s for a full rotation) --> 3200 steps, 1s --> 106 steps) - you can tweak this value with the SpeedCalibration.ino file
#define STEPPER_ACCEL 2000 // step/s/s

AccelStepper stepper = AccelStepper(AccelStepper::DRIVER, PIN_STEP, PIN_DIR);
LiquidCrystal_I2C lcd(0x27, 20, 4); // set the LCD address to 0x27 for a 16 chars and 2 line display

// ADDED BUZZER
constexpr int buzzer =A1;


// custom full black char for blinking
byte fullBlack[] = {
  0x1F,
  0x1F,
  0x1F,
  0x1F,
  0x1F,
  0x1F,
  0x1F,
  0x1F
};

String modes[3] = {"Wash", "Cure", "Dry"}; // availables modes

int highlightMode = 0; // index of the current hilighted mode
int mode = -1; // index of the current mode 

bool modeSelectionState = false;
bool timeSelectionState = false;
bool modeConfigurationState = false;

// encoder variables
int currentStateCLK;
int lastStateCLK;
unsigned long previousClickMillis = 0; // will store last time buttton was pushed (for cancel function)

// variables for blinking mode
unsigned long previousMillis = 0; // will store last time LCD was updated (for blibnking)
const long interval = 500; // interval at which to blink (milliseconds)
bool blinkState = false;

int t = 0; // minute
int remainM = t; // remaining time in minutes

void setup() {
  // LCD init
  lcd.init();
  lcd.backlight();
  lcd.setCursor(1, 0);
  lcd.print(F("Wash&Cure v1.0.1"));
  lcd.setCursor(1, 1);
  lcd.print(F("by Xm0dpt 2022"));
  lcd.createChar(0, fullBlack); // fullBlack char creation
  delay(1000);

  // set LEDs as output
  pinMode(PIN_UVLEDS, OUTPUT);

  // set encoder pins as inputs
  pinMode(CLK, INPUT);
  pinMode(DT, INPUT);
  pinMode(RE_KEY, INPUT);
 
  // BUZZER 
  /**** buzzer pins *****/
  pinMode(buzzer,OUTPUT);

  // set stepper driver pins as outputs
  pinMode(PIN_DIR, OUTPUT);
  pinMode(PIN_STEP, OUTPUT);
  pinMode(PIN_RESET, OUTPUT);
  pinMode(PIN_SLEEP, OUTPUT);
  pinMode(PIN_ENABLE, OUTPUT);

  // set microstepping settings pins as output
  pinMode(MS1, OUTPUT);
  pinMode(MS2, OUTPUT);
  pinMode(MS3, OUTPUT);

  // uvleds are off at start
  digitalWrite(PIN_UVLEDS, LOW);

  // stepper is in sleep mode at start
  digitalWrite(PIN_SLEEP, LOW);
  digitalWrite(PIN_RESET, LOW);
  digitalWrite(PIN_ENABLE, HIGH);

  // set stepper acceleration
  stepper.setAcceleration(STEPPER_ACCEL);

  // read the initial state of CLK
  lastStateCLK = digitalRead(CLK);

  // display available modes
  displayModesMenu();

  
}

void loop() {
 
  // read the rotary encoder to change highlighted mode
  rotaryEncoderRead(highlightMode);

  if (modeSelectionState)
  {
    // set limits of hightlightmode -- only 3 modes availables
    if (highlightMode < 0)
      highlightMode = 0;

    if (highlightMode > 2)
      highlightMode = 2;

    // define offset for blinking start char
    int offset = 0;

    for (int i = 0; i < highlightMode; i++)
      offset += (modes[i].length() + 2);

    // use millis() instead of delay() to not block process
    unsigned long currentMillis = millis();

    if (currentMillis - previousMillis >= interval) {
      // save the last time selected mode was blinked
      previousMillis = currentMillis;

      // inverse blinking state
      blinkState = ! blinkState;

      if (blinkState)
      {
        // write fullBlack char on all letters of the selected mode
        for (int i = 0; i < modes[highlightMode].length(); i++)
        {
          lcd.setCursor(i + offset, 1);
          lcd.write(0); // 0 = custom blackChar
        }
      }
      else
      {
        // reprint all modes normally
        lcd.setCursor(0, 1);
        lcd.print(modes[0] + F("  ") + modes[1] + F("  ") + modes[2]);
      }
    }

    // if user press rotary encoder button
    if (!digitalRead(RE_KEY))
    {
      // wait release of RE_KEY
      while (!digitalRead(RE_KEY));
      // then enter config mode to set duration
      modeConfigurationState = true;
      configureMode(highlightMode);
    }
  }
}

void configureMode(int desiredMode)
{
  modeSelectionState = false; // exit modeSelectionState

  bool clickTimeInited = false;
  lcd.clear();
  lcd.setCursor(3, 0);
  lcd.print(modes[desiredMode] + F(" time"));
  lcd.setCursor(4, 1);
  lcd.print("00 : min");

  while (modeConfigurationState)
  {
    // read and assign new value of t
    int old_t = t; //save t
    rotaryEncoderRead(t);

    // set limits of duration time
    if (t < 0)
      t = 0;

    if (t > 60)
      t = 60;

    // display new value of t
    if (t != old_t)
    {
      lcd.setCursor(4, 1);
      if (t < 10)
      {
        lcd.print("0");
        lcd.setCursor(5, 1);
      }

      lcd.print(t);
    }
    if (!digitalRead(RE_KEY))
    {
      delay(100); //anti-rebound

      if (!clickTimeInited)
      {
        previousClickMillis = millis();
        clickTimeInited = true;
      }

      if (digitalRead(RE_KEY) && (millis() - previousClickMillis <= 1000) && t > 0)
      {
        clickTimeInited = false;
        previousClickMillis = millis();

        mode = desiredMode; // set the desired mode as actual mode
        startMode(mode, t); // start the mode with desired duration
      }
      else if (millis() - previousClickMillis >= 1000)
      {
        clickTimeInited = false;
        previousClickMillis = millis();

        mode = -1; // reinit mode index
        modeConfigurationState = false; // exit configuration mode
        displayModesMenu(); // return to main menu
      }
    }
  }
}

void startMode(int modeIndex, int t)
{
  // Mode 0 : Wash  --> Only motor at fast speed
  // Mode 1 : Cure  --> Motor at slow speed + UVLEDs
  // Mode 2 : Light --> Only UVLEDs (to cure resin residues in alcohool and make them fall to easier filtering)

  // MS1 MS2 MS3 Microstep Resolution
  //  L   L   L      Full step
  //  H   L   L      Half step
  //  L   H   L    Quarter step
  //  H   H   L    Eighth step
  //  H   H   H  Sixteenth step

  modeConfigurationState = false;

  if (modeIndex == 0)
  {
    // WASH MODE -- MOTOR TURN FOR A SPECIFIED TIME. DIRECTION IS REVESED ALL MINUTES

    // Set motor direction clockwise
    digitalWrite(PIN_DIR, HIGH);
    // set sleep and reset at FALSE and enable at TRUE (inverted states here)
    digitalWrite(PIN_SLEEP, HIGH);
    digitalWrite(PIN_RESET, HIGH);
    digitalWrite(PIN_ENABLE, LOW);

    // set full step for high speed
    digitalWrite(MS1, LOW);
    digitalWrite(MS2, LOW);
    digitalWrite(MS3, LOW);

    stepper.setMaxSpeed(MAX_STEPPERSPEED_WASH);

    // set remaining time
    remainM = t;

    float nstep = (float)MAX_STEPPERSPEED_WASH * 60.0; // set a 1min cycle (acceleration is not take account to simplify)

    while (remainM > 0)
    {
      refreshTimeRemaining(); // refresh display
      stepper.move(nstep); // tell stepper to rotate for 1 minute

      while (stepper.distanceToGo() != 0)
      {
        stepper.run();
      }

      // reverse rotation direction
      nstep *= -1;
      // decrease minutes counter
      remainM --;
      // wait a bit to not damage motor
      if (remainM != 0)
        delay(2000);
    }

    handleFinishedMode();

  }
  else if (modeIndex == 1)
  {
      // WASH MODE -- MOTOR TURN FOR A SPECIFIED TIME. DIRECTION IS REVESED ALL MINUTES

    // Set motor direction clockwise
    digitalWrite(PIN_DIR, HIGH);
    // set sleep and reset at FALSE and enable at TRUE (inverted states here)
    digitalWrite(PIN_SLEEP, HIGH);
    digitalWrite(PIN_RESET, HIGH);
    digitalWrite(PIN_ENABLE, LOW);
    
    // turn on UV LED pannel
    digitalWrite(PIN_UVLEDS, HIGH);
    
    // set full step for high speed
    digitalWrite(MS1, HIGH);
    digitalWrite(MS2, HIGH);
    digitalWrite(MS3, HIGH);

    stepper.setMaxSpeed(MAX_STEPPERSPEED_CURE);

    // set remaining time
    remainM = t;

    float nstep = (float)MAX_STEPPERSPEED_WASH * 10.0; // set a 1min cycle (acceleration is not take account to simplify)

    while (remainM > 0)
    {
      refreshTimeRemaining(); // refresh display
      stepper.move(nstep); // tell stepper to rotate for 1 minute

      while (stepper.distanceToGo() != 0)
      {
        stepper.run();
      }

      // reverse rotation direction
      nstep *= -1;
      // decrease minutes counter
      remainM --;
      // wait a bit to not damage motor
      if (remainM != 0)
        delay(2000);
    }

    handleFinishedMode();

  }
  else if (modeIndex == 2)
  {
    // LIGHT MODE - TURN ON UN LED FOR A SPECIFIED TIME
    
    digitalWrite(PIN_UVLEDS, HIGH);

    // set remaining time
    remainM = t;

    // define duration in ms
    unsigned long tt = (unsigned long)remainM * 1000 * 60;

    delay(tt);

    handleFinishedMode();
  }
}

void handleFinishedMode()
{
  lcd.clear();
  lcd.setCursor(6, 1);
  lcd.print(modes[mode]);
  lcd.setCursor(4, 1);
  lcd.print("Finished");

  buzzer_alarm();
  buzzer_alarm();
  buzzer_alarm();

  // enter sleep mode
  digitalWrite(PIN_SLEEP, LOW);
  digitalWrite(PIN_RESET, LOW);
  digitalWrite(PIN_ENABLE, HIGH);
  // turn off uv leds
  digitalWrite(PIN_UVLEDS, LOW);

  // let user get back to main menu by pressing encoder btn
  //wait a press
  while (digitalRead(RE_KEY)) delay(50);
  // wait the release before proceed
  while (!digitalRead(RE_KEY)) delay(50);

  mode = -1; // reinit mode
  t = 0;
  displayModesMenu(); // display modeMenu
}

void printTime(int m)
{
  //print remaining minutes on two digits

  lcd.setCursor(6, 1);

  if (m < 10)
  {
    lcd.print("0");
    lcd.setCursor(7, 1);
  }

  lcd.print(m);
  lcd.setCursor(8, 1);
  lcd.print(" min");
}

void refreshTimeRemaining()
{
  lcd.clear();

  lcd.setCursor(1, 0);
  lcd.print(F("Time"));
  lcd.setCursor(2, 1);
  lcd.setCursor(1, 1);
  lcd.print(F("Left"));
  lcd.setCursor(7, 1);
  //printTime(remainM);
  lcd.print (remainM);
}

void displayModesMenu()
{
  lcd.clear();
  lcd.setCursor(2, 0);
  lcd.print(F("Select mode"));
  lcd.setCursor(0, 1);
  lcd.print(modes[0] + F("  ") + modes[1] + F("  ") + modes[2]);

  modeSelectionState = true;

  // wait the release before continue
  while (!digitalRead(RE_KEY)) delay(50);
}

//modify the passed value according to the rotation direction (CW/CCW)
void rotaryEncoderRead(int &val)
{
  //I wrote nothing myself here
  //CODE FROM : https://lastminuteengineers.com/rotary-encoder-arduino-tutorial/

  // Read the current state of CLK
  currentStateCLK = digitalRead(CLK);

  // If last and current state of CLK are different, then pulse occurred
  // React to only 1 state change to avoid double count
  if (currentStateCLK != lastStateCLK  && currentStateCLK == 1) {

    // If the DT state is different than the CLK state then
    // the encoder is rotating CCW so decrement
    if (digitalRead(DT) != currentStateCLK) {
      val --;

    } else {
      // Encoder is rotating CW so increment
      val ++;
    }
  }

  // Remember last CLK state
  lastStateCLK = currentStateCLK;
}
void buzzer_alarm() {
    // Sounds the buzzer at the frequency relative to the note C in Hz
    tone(buzzer,5000, 500);
    // Waits some time to turn off
    delay(1100);
    //Turns the buzzer off
    noTone(buzzer);
    // Sounds the buzzer at the frequency relative to the note D in Hz
   return;
}

Really, no chance for you to modify this? It's too much work to do the entire mod for free. Sorry. I could definitely give you suggestions but you have ruled that out.

Hello aarg

thank you for the reply.

the problem is i don´t know where to beggin.....

But thank you any way

I began by loading and running the blink sketch. That was about 2015. In two months, I had a working wireless weather station made with Arduinos. I followed some tutorials, but really, mostly I just read documentation and tried different things.

ok thank you for the "HINT"

what i came here to do was to get some pointers for my issue.... not the full solution... and definitly not for sarcasam

but thank you anyway

I don't see any sarcasm. Can you please quote it?

You said, "i lack the skill to merge/adapt any of them to my proj" If you lack the skill, how are you going to use "pointers"?

I looked at the sketch, it's organized to count minutes, so changing it isn't easy.

as i said and you quoted... "i lack the skill to merge/adapt any of them to my proj"

my lack expression sujestes that i could do some of the things but not all.. meening not "dumb" ( not acusing anyone to call me that) however i also used the word adapt and with that i am asking for some one to take a look at the code and help me adapt/correct by giving sugestions in how to or where to look and not awnsers like "... mate go back to school and start from "0"..." type of things.

i also said that i have tryied before posting anything by using google.... youtube... this and other forums... github ... etc...

in sum... i am not a total "ZERO" but also i am not, and i quote "programer".

but like everyone where... we will get there some day....

thank you

Okay, let's run with that. Have you spent some time reading the code?

Although... you did say, " the skill to merge/adapt any of them..."

I can't read your mind, just what you say.

This program is commented, luckily... look at this variable, do a "find" operation and see how it's used wherever it appears in the code...

int remainM = t; // remaining time in minutes

Probably, you should change it to seconds, so it has to be both updated and used as seconds rather than minutes...

Imma guess @anon57585045 got as far as I did right away and found

    while (remainM > 0)
    {
      refreshTimeRemaining(); // refresh display
      stepper.move(nstep); // tell stepper to rotate for 1 minute

      while (stepper.distanceToGo() != 0)
      {
        stepper.run();
      }

Which blocks your code here for one minute, that there is nothing ticking off the seconds in the code at all.

And this means that there will have to be a change in strategy in the above code, and anything else in the code (stop looking myself, too) that blocks like this and goes dark.

You could hack in the seconds here, or make a proxy that looked like the timing was being performed otherwise than blocking for "minutes" at a time. Which might look accurate enough but would be a fiction.

I think @anon57585045 said so I may be repeating: the code should be organized around the smaelst period of time you care about, 1 second here, and every second the code should

  • look at the inputs
  • perform some logic based on those inputs and the current "state" of the "machine" and then
  • set any output appropriate.

Rinse and repeat. Every second.

HTH

a7

let me start by sayig thank you to both for the replys

@anon57585045
i read the comments and got as far as realisinf "remainigM" was the var the stores the value "minuts" and i couldn't get my head arroud as all the other samples i have don't have this type of coding ( i know... there a 100000000 ways to skin a cat ).

@alto777
thank you and now i have something to look for with your hint and @anon57585045 hint.....

let's see how far i can get

thank you both

this was what i found stange and i don't know where to start to look for it
....but i am very new to the millis and how the code counts time

as i said... the code was not done by me... i am just trying to adapt what is allready there and lear as i go....

A clue we see as to where things mightn't be done the best way is the while statement.

In the code (we are going to call it your code even though you haven't made it yours yet) there are two instances of

    while (remainM > 0) {

which hang because remainM is reduced by 1 every time you tell the stepper to run for a minute then wait for it to do that.

a7

the way it's workig is... the are 3 options that do dif things

on option 1 (wash), it spins the motor for "x" ammount of time (pre selected) and during that time it changes direction every min and relay is OFF

lets say 5 min selection.... rotates left 1 min -stops - rotates right 1 min - stops....etc

on option 2 (cure), the motor spins very slowly in the same direction for the amount of time selected and the relay is on

on option 3 the motor is stoped and only the relay is on for the amount of time selected

i also have the code for showing a timer from another proj but i think i am missing something

/**** show countdown time *****/
void showTime(unsigned long timeLeft) {
  static uint8_t lastsecs=-1;
  static char szString[6];
  uint16_t secs=(uint16_t)(timeLeft/1000ul);
  if( secs != lastsecs )  {
    lastsecs = secs;
    u8g2.setCursor( 20,45 );
    sprintf( szString, "%02d:%02d", secs/60, secs%60 );
    // Serial.println( szString );
    u8g2.clearBuffer();
    u8g2.print( szString );
    u8g2.sendBuffer();
  }
}

it is using an oled ( not a problem)

what i am trying to do is to understand how it works so i can find waht i can do

it has a var "timeLeft" where it has the time remaining but nest has another one "lastsecs=-1" the takes -1 from the last stored sec... am i right ?

That's perfectly good code for displaying time being kept to the second.

lastsecs is starts at -1 so it won't match, and is subsequently used to display the time only when the current seconds are different to the previous ly displayed seconds (lastsecs).

Which means it can be called as frequently as you like, but it only does anything when it needs to.

This is, in essence, what your code must do. Be written (I'd say rewritten, but it's kinda major) so that every piece of logic can be called as frequently as you want, but it will only do anything when it is necessary.

At least the parts where you are standing there watching time go by, not adjusting times or selecting modes or any other "interactive" elements.

Although they, too, could be written to constantly scan inputs, make decisions and produce outputs.

The general concept is the FSM or "finite state machine" which this kind of thing can be coded using.

Maybe take a break away from the actual project, slow your role a bit and google

arduino finite state machine

or the classic

arduino finite state machine traffic lights

FSMs use well-known programming patterns and there are literally dozens of examples, tutorials, guides and so forth. Read a few of them for a few minutes and see if any match your mood and current state of knowledfge.

I just noticed

    remainM = t;

    // define duration in ms
    unsigned long tt = (unsigned long)remainM * 1000 * 60;

    delay(tt);

which also complicate things, as will any use of delay() for doing timing. It is the same problem as while ing away a minute, so to speak.

a7

in other LAME/N00b word ... i am F#"$% with this and i thought this could have an easy solution/adaptation/conversion

i am going to take your advice and take a look at the FSM and see where i can get with this

once again thank you