Non-blocking countdown sport timer implementation question

Hello,
i need to implement a "deceptively" simple sport countdown timer(LED display) but i'm wrecking my head for the past days without much working code, i have a flowchart(in good old paper but transferring that to code has been.. too much of a challenge), the entire timer step is like this:

you push [start/reset] button

  1. BEEP (beeps for 1 second, this is a simple output)
  2. 10 second countdown
  3. BEEP
  4. 180 second countdown (shift A)
  5. BEEP
  6. 10 second countdown again
  7. BEEP
  8. 180 second countdown with all decimal points lit (shift B)
  9. BEEP 3 times
  10. Goes back to standby with 3 dashes on display(could be displaying other stuff)

If at any point after starting the button is pressed, then it has to BEEP 3 times(as it would finish) and remain in waiting. Whilst beeping the time can keep counting.

The problem is that the code has to be fully non-blocking as this will also interface via BLE.

The non-blocking part is what's killing me, i'm having problems even with the beeper part of it, i'm trying to implement as much reusable functions as i can but i don't want to end up with a ton of ¿unnecessary? intermediate flags/variables either.

for the beep my idea was to make a beep() function that's called in loop{} and test against a beeping flag set at the end of each interval.

As an example this is the snippet for the beep function i made:

//vars
unsigned long currentmillis = 0;

void loop() {
  currentmillis = millis();
  beeper();
}

void beeper() {
  //lee si el flag de beep está en 1
  unsigned long beepmillis;
  if (beeping == true) {
    digitalWrite(LED_BUILTIN, HIGH);
    beepmillis = millis();
    isbeeping = true;
    beeping = false;
  }
  if (isbeeping == true && currentmillis - beepmillis >= 1000UL) {
    isbeeping = false;
    digitalWrite(LED_BUILTIN, LOW);
  }
}

i haven't even managed to tackle the counting one yet...

i've also taken a look at the Neotimer library, in which i'd start a instance with 1000mS, then everytime it's done i decrement the countdown variable. ¿what do you guys think?

try..
UnoBlink

//vars
unsigned long currentmillis = 0;
unsigned long beepmillis;
bool beeping = true;

void setup() {
  Serial.begin(115200);
  Serial.println("ready..");
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  currentmillis = millis();
  beeper();
}

void beeper() {
  //lee si el flag de beep está en 1
  if (beeping == true && currentmillis - beepmillis >= 1000UL) {
    digitalWrite(LED_BUILTIN, HIGH);
    beepmillis = millis();
    beeping = false;
  } else if ( currentmillis - beepmillis >= 1000UL) {
    beepmillis = millis();
    beeping = true;
    digitalWrite(LED_BUILTIN, LOW);
  }
}

have fun.. ~q

1 Like

Before reading your code i was testing mine and it was failing, after reading your code i see you moved the "unsigned long beepmillis;" to global, after doing that my code started working perfectly.

So this "solves" the beeping part of the code, ¿what about the rest of the core code?

simple state machine, using a switch case..
i'll add one to the sim, 2 secs..

~q

With a few changes this code should work for you. Anything you add needs to be non blocking. It does nothing but send a few messages to the console to demonstrate what it does. I wrote it many years ago in assembler, just translated it to CPP about ten years ago for my use on the arduino. Let me know how it works out for you.



char Gil[] = "\n\tVersion: 1.1A 230617";     // This is printed in the hello message at setup
/**************************************[ FYI ]***********************************************
**                                                                                         **
**  This Code was compiled with IDE 1.8.13 on AMD processor running Linux Mint 18.3 Sylvia **
**  This core module uses a state sequencer (threads) to control the processes and there   **
**  pseudo priority and processing rate. this structure is such as the higher the sequence **
**  number the higher pseudo priority of the task(higher occurs first). Also contained in  **
**  this Base Module is Gil_Timer() Great Industrious Little _Timer. Although not super    **
**  accurate it will work for a lot of applications and keep the mills portion simple, it  **
**  is now based on mills rather then interrupts. This Great Industrious Little timer      **
**  uses 16 bit integers in place of unsigned 32 bit numbers. This can save a lot of RAM   **
**  Depending on the number of timers implemented. The time base can be changed at will by **
**  changing Tick. This can be done in the program by changing to int Tick = #;            **
**                                                                                         **
**     I have used this on Arduino (Nano, UNO, Mega, ESP8266 ) OEM and China versions.     **
**  This code is written in the way I like it. I have a few years programming in various   **
**  various assemblers, I am now in the process of learning the higher level languages.    **
**  If you do not like the format either change it or do not use it! The original of this  **
**  was written in about 1981 in assembler so it is creaky old.                            **
**   Gil Shultz 200614  -  June 14, 2020                                                   **
*********************************************************************************************/

// Set this up at the beginning of the program before setup() or loop()
/***************************[ Great Industrious Little Timer ]************************************
 *                                                                                               *
 *   This defines the offset (Gil Timer Number) into the timing array. Each of these timers is   *
 *   counted down x times a second, when zero is reached the timer is no longer decremented.     *
 *   To use the timer simply place the delay time you want (16 bit unsigned integer) in the      *
 *   appropriate Array location and check for zero which indicates the time has expired. The     *
 *   only limit is the amount of Ram available and the amount of time available for servicing    *
 *   the pseudo interrupt. The following constructs are offsets into the timer array Gil_Timers  *
 *   "num_Gil_Timers" determines the number of timers that will be serviced. You can increase or *
 *   decrease the number This code has been modified to use the millis() function in militime()  *
 *   Currently we are set up to count 0.01 seconds so 500 = 5 seconds, 100 = 1 second.           *
 *      140212                                                                                   *
 *****************[ This has been modified from an Arduino interrupt ISR ]***********************/
const  int     num_Timers          =      10; // Number of Timers Zero used for Debug
int Gil_Timer[num_Timers]          =     {0}; // Array defining the storage location for the timers.
const  int        BugTimer         =       0; // This timer is reserved for Debugging, do not use
const  uint16_t   BugTime          =     500; // Temporary time to be changed as needed
const  int        Alrm_Timer       =       1; // offset into array for alarm timer
const  uint16_t   Alrm_Time        =     100; // Alarm blink Delay 100 = 1 second or 0.01
const  int        Case_Timer       =       2; // offset in array for the Case Timer
const  uint16_t   Case_Time        =     150; // Delay time for the case timer
const  int        A_test_timer     =       3;
const  uint16_t   A_test_time      =     100; // These are samples that do nothing but print a number
const  int        B_test_timer     =       4; // on the console. It tries to demonstrate that the
const  uint16_t   B_test_time      =      90; // timers although have the same time base operate
const  int        C_test_timer     =       5; // independently from each other.
const  uint16_t   C_test_time      =      80;
const  int        D_test_timer     =       6;
const  uint16_t   D_test_time      =     70;
const  int        E_test_timer     =       7;
const  uint16_t   E_test_time      =     150;
const  int        F_test_timer     =       8;
const  uint16_t   F_test_time      =     150;
const  int        G_test_timer     =       9;
const  uint16_t   G_test_time      =     150;
// Fill in with additional Timers!

/***********************************************************************************************
 *                                                                                             *
 *  Tick Timers count up from 1 to 0x0 (overflow to zero). If the value is zero the timer is   *
 *  idle. Nice for elapsed timing, debouncing etc. This uses the same time base and Gil Timer  *
 *    230615                                                                                   *
 ***********************************************************************************************/
const  int     num_Tickers         =       4; // These are used to measure elapsed time they start at
int Gil_Ticker[num_Tickers]        =     {0}; // This is the number of timers, it should match below
const  uint8_t     A_TicTimer      =       0; // Timer offset into array
const  uint16_t     A_TicTime      =       0; // If zero it is ignored
const  uint8_t     B_TicTimer      =       0; // Timer offset into array
const  uint16_t     B_TicTime      =       0; // If zero it is ignored
const  uint8_t    C_TicTimer       =       0; // Timer offset into array
const  uint16_t     C_TicTime      =       0; // If zero it is ignored
const  uint8_t     D_TicTimer      =       0; // Timer offset into array
const  uint16_t     D_TicTime      =       0; // If zero it is ignored

// Change to: int Tick;  for your code to dynamically change the time base.
const  int     Tick                =      10; // Time base for Gil_Timers 1=1ms 10=10ms 100=100ms
unsigned long  Last_Time           =       0; // This is part of the time base for Gil_Timers

// State equates
const int Max_State                =      50; // Maximum number of states (Threads)
int stateM                         =       0; // Our state machine counter

// Hardware Equates    I always use a test pin as it makes debugging easer
const   int      TestPin           =       7; // This is pinned as D2 on my Arduino D1 R2 Board

//***********[ Location and ID information ] ************************************
char  Revision_Date[] =  "Revision\0\0";  // This is a dummy for the revision Number
char  TmpBuffer[16] {0};                  // Used for string calculations

//***************[ These are used for the console Hello Message ]****************************
const char  Source_file[]          =    __FILE__;  // Main File Name
const char  compile_date[]         = __DATE__ " " __TIME__; // Time and Date from Complier

// Put this with your Functions

//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@[ Functions]@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
/******************************[ Functions Installed ]****************************************
*                                                                                            *
*   militime()             + used to replace interrupt timer base.                           *
*   Mark_It()              + Generates a pulse on the Test Pin, used for timing etc.         *
*   logname(1,2,3)         + Configure year (4 dig) month day as a string. Use compiler info *
*   LastLine()             + Used as part of the serial number must be at end                *
*                                                                                            *
*   ISR(TIMER1_COMPA_vect) + This is the Gtimer interrupt replaced with millies              *
*                                                                                            *
****************************[ Listed in program order ]***************************************/



/****************************[ Process the software timers here ]**************************
*                                                                                         *
*          Timers[?] is processed Here and called from the main loop                      *
*                                                                                         *
*   This updates the (Gil_Timers) every 0.1 second called in the main loop by utilizing   *
*   the Mills function. This then decrements each timer every time period until they      *
*   reach zero. This function yielding almost an unlimited number of timers only          *
*   constrained by the amount of memory available.                                        *
*                                                                                         *
*   Set timers[?] to the delay time wanted see Tick for period. The value at timers[?]    *
*   has the time remaining until time out (tick). The timer will will stop at zero when   *
*   timed out. It is the responsibility  of the application to check  the  timers[?] for  *
*   zero. Note there is some jitter because of interrupts etc. This Unlike the "delay()"  *
*   function is non blocking. This was derived from my ISR with the same name.            *
*                                                                                         *
*   The Tickers timers will count up unsigned from 1 through 0, the overflow causes zero  *
*                                                                                0.14 us  *
*******************[ Note these Gil_Timers are NOT for critical timing]********************/
void militime() {
  unsigned long currentTime = millis();       //  Updates frequently
  if (currentTime - Last_Time >= Tick) {      // This is the time base for Gil_Timers
    int i;  int x;                            // Temporary save RAM
    for (i = 0; i < num_Timers; i++) {        // Initialize our loop, i is our timer pointer
      x = Gil_Timer[i];                       // Get the Timer we are pointing at
      if (x > 00) {                           // If zero we are finished, if not decrement it by 1
        x--;                                  // if not subtract a second
        (Gil_Timer[i]) = x;                   // and update the timer location
      }           // End of X > zero
    }          // End of for i loop
    Last_Time = currentTime;                  // Update the timing for the next time around
    // ******  Tickers
    for (uint8_t i = 0; i < num_Tickers; i++) { // Initialize our loop, i is our timer pointer
      uint16_t x = Gil_Ticker[i];             // Get the Timer we are pointing at
      if (x > 00) {                           // It will roll over and stop at zero
        x++;                                  // if not add a tick
        (Gil_Ticker[i]) = x;                  // and update the timer location
      }           // End of X > zero
    }          // End of for i loop
  }      // end of event time test
}     // End of militime


/*******************************************************
 *                                                     *
 *   Send Marker to trigger Logic Analyzer or Scope.   *
 *                                                     *
 *******************************************************/
void Mark_It() {
  //  if (Gil_Timer[Alrm_Timer] == 0) {       // Loop Delay
  //    Gil_Timer[Alrm_Timer] = Alrm_Time;    // You can increase the time when using LEDs
  digitalWrite(TestPin, HIGH);                // For me this is normally pin 7, no reason for that
  // } else {
  digitalWrite(TestPin, LOW);                 // That completes the pulse/
  //  }    // End of Else
}  // End of Mark_It

/*******************************************************************
*                                                                  *
*  Configure year (4 dig) month day as a string. Use compiler info *
*  Print with Buffer +2 to eliminate the century.                  *
*                                                                  *
********************************************************************/
void logname(char const *date, char *buff) {
  int month, day, year;
  static const char month_names[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
  sscanf(date, "%s %d %d", buff, &day, &year);
  month = (strstr(month_names, buff) - month_names) / 3 + 1;
  sprintf(buff, "%d%02d%02d.txt", year, month, day);
}

//@@@@@@@@@@@@@@@@@@@@@@@@@[ Code Setup ]@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
/***********************[ Our code starts here }************************
 **                                                                   **
 **    This is where we define our starting point This is entered     **
 **    only once when the processor starts Example, Startup or reset  **
 **    button or from shield This way we start with everything in a   **
 **    known state                                                    **
 **                                                                   **
 **                put your setup code here, to run once.             **
 **                                                                   **
 ***********************************************************************/
void setup() {                                // put your setup code here, to run once:
  Serial.begin(115200);                       // This is for Debug
  pinMode(TestPin, OUTPUT);                   // Set up our TestPin as an output

  //************  Set up Revision Date Buffer  (Part of Setup) ***************
  // This contains the Date as YYMMDD and the last line number LLN so we get YYMMDDLLN
  logname(__DATE__, TmpBuffer);               // Get date string
  for (int i = 0; i < 8; i++) {               // Initialize our loop, to move yyyymmdd (8 Characters)
    Revision_Date[i] = TmpBuffer[i];
    Revision_Date[i + 1] = 0;                 // Add tag for end of string
  }    // End of for i loop buffer parsed
  Serial.print(F("\n\n\n\n\n\n\t\t Hello Gil!"));// Clean up the screen a bit
  Serial.print(F("\n\tThis Program is filed as:\n"));// Make room to tell who we are including the path
  Serial.print(Source_file);                  // Display the source file name
  Serial.print(Gil);                          // Located at the begining of the file.
  Serial.print(F("\n\tRevision: "));          // Show the rest of the info such as compiled date
  Serial.print(Revision_Date + 2);            // Skip past the century (YYYYMMDD)
  Serial.print(LastLine());                   // Show the current revision date inserted manually
  Serial.print(F("\n\tcompiled: "));          // Show date of compilation
  Serial.print(compile_date);                 // Get it from the compiler data
  Serial.print(F("\n\n"));                    // Show date of compilation
  delay(5000);                                // To prevent serial freeze in uno
  Gil_Ticker[A_TicTimer] = 0x0001;            // Start our up ticker and let it wrap around to zero
}   // End if Setup

//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@[ Main Code ]@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
/***************************************************************************************************
*                                                                                                  *
*         The Main Program loop is here. It. dispatches to all Sub Processes!                      *
*   NOTE: Loop delays will Kill militime which needs to be cycled each pass through your loop.     *
*  On My Arduino D1 R3 (ESP8266)) This loop cycles 65,800 times a second however this will change  *
*  if the code is changed. Adding or removing process will change the time of the main loop.       *
*  This is not prememtptive so blocking code will stall the loop. This works nice when combining   *
*  several different codes.                                                                        *
*     or 0.000015198 of a second per pass. This changes with clocks and processors.                *
*                                                                                                  *
*                                                                                                  *
****************************************************************************************************/
void loop() {
  //  wdt_reset();                            // Pet the dog so it does not bite!
  //  militime() is our pseudo timer for software delays. This must run every pass of the loop
  militime();                                 // Cycle our program timer for events
  Mark_It();                                  // Send pulse to Logic analyzer for timing

  /*************************[ Program States ]*************************************
   **                                                                            **
   **      The is the main program loop, it controls all main functions          **
   **       Implementing a pseudo state machine.  Utilizing states               **
   **       accessed by using this statement: [if (stateM == ?) {}               **
   **  State:                                                                    **
   **     3.  Test Timer 3      Just for Demo and in no order                    **
   **     5.  Test Timer 5      Just for Demo and in no order                    **
   **    13.  Test Timer 13     Just for Demo and in no order                    **
   **    22.  Test Timer 22     Just for Demo and in no order                    **
   **    44.  Test Timer G      Just for Demo and in no order                    **
   **    43.  Test Timer I      Just for Demo and in no order                    **
   **    42   Test Timer L      Just for Demo and in no order                    **
   **    25.  Test Timer Case   Just and gives us a clean line.                  **
   **        50. ----------------------------                                    **
   **   stateCount  -1 update PWM channels (Currently 50 states)                 **
   **   NOTE: States can be in any order in the source code but will be executed **
   **  in numerical sequence. To disable a state simply change its state number  **
   **  to something > Max_States.You can call a function as many times as wanted **
   **  it will process OK. Careful of Variables and redefining them.             **
   **  When combing programs you can place them as a pseudo thread and it will   **
   **  be processed each time through the loop. They cannot have the name loop.  **
   **  The code must be non blocking ie. NO delay()statements as they will block **
   **  your code and nothing else will run. Waiting for something is forbidden   **
   **  as that will block. Test and if not true exit and test next time through. **
   **                                                                            **
   ********************************************************************************/

  // Example of a disabled state, There are only Max_State states and we do not use 0 - or Max_State
  if (stateM == 61) {
    digitalWrite(TestPin, LOW);               // Debug;
  }

  //****[ Sample of a thread with independent timer ]****
  if (stateM == 3) {                          // Test for state, If not valid this is skipped.
    //  Mark_It();                            // Used to time function and when active
    if (Gil_Timer[A_test_timer] == 0) {       // Check if it has timed out.
      Gil_Timer[A_test_timer] = A_test_time;  // Yes, just reset it to the correct delay
      Serial.print(F("#3 "));                 // Our code controlled by processor and a timer
    }
    //  Mark_It();                            // For accurate measure from the falling of first to rising here
  }    //  End of stateM == 3

  //***********
  if (stateM == 5) {
    if (Gil_Timer[B_test_timer] == 0) {
      Gil_Timer[B_test_timer] = A_test_time;
      Serial.print(F("#5 "));
    }
  }//  End of stateM == 5

  //***********
  if (stateM == 13) {
    if (Gil_Timer[C_test_timer] == 0) {
      Gil_Timer[C_test_timer] = A_test_time;
      Serial.print(F("#13 "));
    }
  }//  End of stateM == 13

  //***********
  if (stateM == 22) {
    if (Gil_Timer[D_test_timer] == 0) {
      Gil_Timer[D_test_timer] = A_test_time;
      Serial.print(F("#22 "));
    }
  }//  End of stateM == 22

  //***********
  if (stateM == 44) {
    if (Gil_Timer[E_test_timer] == 0) {
      Gil_Timer[E_test_timer] = A_test_time;
      Serial.print(F("#G "));
    }
  }//  End of stateM == 44

  //***********
  if (stateM == 43) {
    if (Gil_Timer[F_test_timer] == 0) {
      Gil_Timer[F_test_timer] = A_test_time;
      Serial.print(F("#I "));
    }
  }//  End of stateM == 43

  //***********
  if (stateM == 42) {
    if (Gil_Timer[G_test_timer] == 0) {
      Gil_Timer[G_test_timer] = A_test_time;
      Serial.print(F("#L --> "));
      uint16_t g = Gil_Ticker[A_TicTimer];    // Force it as unsigned.
      Serial.print(g , HEX);
      Serial.print(F(" "));
    }
  }  //  End of stateM == 43

//***********
// Example of a disabled state
  if (stateM == 625) {                        // Number is greater then Max_State
    digitalWrite(TestPin, HIGH);              // A single thread is taking about 8uS
  }

  /**************[ Display Case Temperature  ]**************
  *       State: #25                                       *
  *    Update the displays on a regular basis.             *
  *   200215                                               *
  **********************************************************/
  if (stateM == 25) {                         // Check our thread - Remove if you are not using threads
    if (Gil_Timer[Case_Timer] == 0) {         // See if we timed out
      Gil_Timer[Case_Timer] = Case_Time;      // Yes reset timer and process it
      Serial.print(F("\nCase: "));
      Mark_It();                              // Function code removed
    }  // End of if timers
  }   // End of stateM == 2                   // Remove if you are not using threads

  /*@@@@@@@@@@@@@@@@@@@@@@@@@@@[ State Machine ]@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
    @@                                                                      @@
    @@   This is the state count controller for the pseudo state machine.   @@
    @@    The state machine is counted down.  This must be at the end of    @@
    @@    the main loop!                                                    @@
    @@                                                                      @@
    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*/
  if (stateM > 0)  {                          // Check current state
    (stateM = (stateM - 1));                  // If greater then 0 subtract 1
  } else {
    (stateM = (Max_State))  ;                 // Reset to start loop over
  }   // End of State Update                  // If zero update count
//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

}   // End of Main Loop


/**********************[ Used as part of the Revision number]************************
 *                                                                                  *
 *  This allows us to use the last line number to be used as part of the revision   *
 *  number. It is used only once during setup. Revision = Last Line/YY/MM/DD        *
 *                                                                         --us     *
 ********[ THIS MUST BE AT THE END OF THE OF THE PROGRAM! to operate properly]*******/
int LastLine() {
  int x = __LINE__;                           // This returns the current line number
  return x + 2;                               // Got to get it correct
}                                             // END OF THE PROGRAM!

I believe I have 15 independent timers running concurrently, You can have as many or as few as memory allows. Look at state 3, if the timer is not zero it exits and goes to the next spot in the program. If it is zero, it does its thing, resets the timer and goes to the next spot. Many times I write code as a program, debug it then make it a process in this program, very easy to do.

1 Like

sim updated..
should be able to expand easy..

//vars
unsigned long currentmillis = 0;
unsigned long beepmillis;
unsigned long lastWait = 0;
byte currentState = 0;
bool beeping = true;

void setup() {
  Serial.begin(115200);
  Serial.println("ready..");
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {

  currentmillis = millis();
  if (currentState > 6) currentState = 0;
  switch (currentState) {
    case 0: beeper(); break;
    case 1: lastWait = millis(); currentState++; break;
    case 2: wait(5000); break;
    case 3: beeper(); break;
    case 4: lastWait = millis(); currentState++; break;
    case 5: wait(5000); break;
    case 6: beeper(); break;
  }
}

void wait(int m) {
  if (currentmillis - lastWait >= m) {
    lastWait = millis();
    currentState++;
  }
}

void beeper() {
  //lee si el flag de beep está en 1
  if (beeping == true && currentmillis - beepmillis >= 1000UL) {
    digitalWrite(LED_BUILTIN, HIGH);
    beepmillis = millis();
    beeping = false;
  } else if ( currentmillis - beepmillis >= 1000UL) {
    beepmillis = millis();
    beeping = true;
    digitalWrite(LED_BUILTIN, LOW);
    currentState++;
  }
}

have fun.. ~q

2 Likes

This tutorial is not a direct example of counting down and beeping but explains basic principles how non-blocking code must be written

and this tutorial explains how to code non-blocking to perform a sequence of actions

In your first posting you wrote such a list with a sequence of actions

you can see each number as a certain mode of operation.
mode 1: BEEP (beeps for 1 second, this is a simple output)
mode 2: 10 second countdown
etc.

In programming some highly sophisticated scientist gave these modes of operation the name

state

and the code-structure that is executing different parts of the code depending on each "state" is called a state-machine
here is a tutorial how to use state-machines

best regards Stefan

1 Like

i've been reading your examples and the examples qubits has pasted but i still haven't been able to make it work, i easily go down rabbit holes with exceptions and timing.

One of your examples with the sequential timers is close, but i still need to include the beeping code and the escape, plus the display code that updates the display every second.

So on one hand i'm not sure if using your "TimePeriodIsOver" function directly to the total time for each segment is a good idea for my situation(i don't think so, it would make displaying the time complicated or imprecise), or i have to use it as a 1 second timer to decrement the displayed value everytime it completes.

but if i use that function to recursively count 10 times, i'm not sure how to initialize the timestamp(unless i re-set the stamp every time the timer expires...)

Still testing stuff and i think i'm very slowly getting there, i think....

From your initial description
each of these 10 steps can be one state.
state 1
state 2,
...
state 10

It might be that it can be reduced. But keep the 10 states as a first step.

You have this ten steps.
Additionally you want to abort the sequence at any time by pressing a button
you would check for the button-press before the state-machine and if button is pressed change

to the state9 (beep 3 times)

The demo-code that changes between

This state

case BlinkLetterOnTheRight:
      if ( TimePeriodIsOver(BlinkTimer, 1000) ) { // check if 1000 milliseconds have passed by
        // if 1000 milliseconds REALLY have passed by
        Change_XO();
        Count_XO_Changes++;  // increment counter-variable by 1
      }
      if (Count_XO_Changes >= 10) {   // check if 10 changes have been done
        // if 10 changes REALLY have been done
        //Count_AZ_Changes = 0;               // reset AZ_Counter
        //Count_XO_Changes = 0;               // reset XO_Counter
        WaitTimer = millis();               // store snapshot of time
        myStateVar = WaitBeforeRepeating;   // change to next state
      }
      break; // immidiately jump down to END-OF-SWITCH

does exactly what you need for the count-down

sure !

In my example a variable is incremented once every second

You would change it to start at 10 or 180 and decrement by one each second

The deviation is maybe 0,0002 seconds per second. Is this too much?

Displaying and updating the time is very very simple:
each time the variable is decremented
update your time-display with the new (decremented number)

There is no resetting the variable in the sense of setting it to zero.
The variable is updated. And this is the exact right thing to do

millis() is continiuosly counting up
Like daily time
put one pizza for baking in the oven at 13:05 o clock baking time 10 minutes
take out at 13:15

put in next pizza at 13:16 baking time again 10 minutes
take out at 13:26
The time is measured against the always updated startTime not against zero

You should read each line of code much more carefully and trying to understand what each line is doing instead of doing quick cross-reading with making assumptions

The function TimePeriodIsOver() updates the timestamp automatically inside the function itself

startOfPeriod = currentMillis; // a new period starts right here so set new starttime

Your initial description says
do beeping - if beeping has finished
do a 10 seconds countdown without beeping
do beeping - if beeping has finished
do a 180 seconds countdown without beeping

is this a correct description?
or
do you want beeing in parallel to the count-down?
Then you have to describe this more precise

2 Likes

Stefan,
below the steps i did mention that the beep should be concurrrent (it could be either way).

After a lot of fiddling i've managed to make it work, BUT i'm having a weird issue with the display and the beeping function ended up being a horrible kludge with intermediate flags all over the place to make it work for the 3 beep at the end, i don't really like how it came out but it's working...

The problem with the display is that the countdown on TWO of the cases(the 10s intermediate steps case comienzo_10: and case intermedio_10:) both go from 2 to 0 instantly it's like it's skipping displaying 1, even when the serial monitor shows it going to 1. I don't know why as i'm using the exact same countdown code from the big countdowns.
I even included display functions for when the variable is checked to 0 to display the 0 -which it does-.

¿could it be that because tiempo_turno is decremented after the display, once it reaches 0 i don't have another round of timing that's why it ends up skipping 2 to 0 super fast?.

If i move the display function AFTER decrementing then it does go from 1 to 0 in 1second, but i never display the starting value:

    case comienzo_10:
      // timer de 10 segundos preparacion inicial
      if (TimePeriodIsOver(waiting10_timer, 1000)) {
        // tick cada 1 segundo
        tiempo_preparacion--;
        dbg("Tiempo restante preparación ", tiempo_preparacion);
        display.showString("PR", 2, 0);
        display.showNumber(tiempo_preparacion, false, 2, 2);
        
      }
      if (tiempo_preparacion == 0) {
        display.showNumber(tiempo_preparacion, false, 2, 2);
        beep_req = true;                //pedir beep
        tiempo_turno = duracion_turno;   //init duración turno
        beeptimestamp = millis();       //timestamp beeper
        waiting180_timer = millis();    //timestamp turno A
        //display.clear();
        myState = turno_a;
      }
      break;

What's weird to me is that i'm using the exact same ticking/countdown code for all counting cases, but the weird behavior only happens on those two i mentioned.

Here's the "buggy" code, i shortened the 180s to 5s for debugging

/*
  ref https://forum.arduino.cc/t/a-demo-code-explaining-the-switch-case-state-machine-and-how-to-do-things-almost-in-parallel/888172

*/
#define dbg(myFixedText, variableName) \
  Serial.print( F(#myFixedText " "  #variableName"=") ); \
  Serial.println(variableName);
// usage: dbg("1:my fixed text",myVariable);
// myVariable can be any variable or expression that is defined in scope

#define dbgi(myFixedText, variableName,timeInterval) \
  { \
    static unsigned long intervalStartTime; \
    if ( millis() - intervalStartTime >= timeInterval ){ \
      intervalStartTime = millis(); \
      Serial.print( F(#myFixedText " "  #variableName"=") ); \
      Serial.println(variableName); \
    } \
  }

//includes
#include <Bounce2.h>
#include <TM1637TinyDisplay.h>

// the states for the state-machine
const byte standby = 0;
const byte comienzo_10 = 1;
const byte turno_a = 2;
const byte intermedio_10 = 3;
const byte turno_b = 4;
const byte triple_beep = 5;

byte myState = standby;  //estado inicial

//constantes
const unsigned int duracion_turno = 5; //180
const int pin_boton = 2;
const PROGMEM char FlashString[] = "ERTA TIMER";

//vars
byte tiempo_preparacion = 10;     //contador 10s, resetear al llamarlo
int tiempo_turno = duracion_turno;    //variable contador tiempo de turnos
unsigned long waiting10_timer;    //timestamp inicio de 10s
unsigned long waiting180_timer;   //timestamp inicio de 180s
unsigned long beeptimestamp;      //timestamp inicio de beeper
unsigned long triplebeeptimestamp;  //timestamp inicio de triple beep final
bool beep_req = false;
bool beeping;
byte beepcount;

//instancias
Bounce boton = Bounce();  //instancing bounce
TM1637TinyDisplay display(4, 3); //clock pin 4, data 3

void setup() {
  boton.attach(pin_boton, INPUT_PULLUP);  // Attach the debouncer to a pin with INPUT_PULLUP mode
  boton.interval(25);                     // Use a debounce interval of 25 milliseconds
  pinMode(LED_BUILTIN, OUTPUT);           // Setup the LED
  display.begin();                        // init display
  display.clear();
  Serial.begin(115200);
  Serial.println("ERTA TIMER v0.2");  //insertar version
}


void loop() {
  boton.update();  // Update the Bounce instance

  if (boton.fell()) {  // Call code if button transitions from HIGH to LOW
    if (myState == standby) {
      Serial.println("boton presionado, iniciar");
      waiting10_timer = millis();  //timestamp para inicio de preparación
      beep_req = true;
      beeptimestamp = millis();       //timestamp beeper
      myState = comienzo_10;
      tiempo_preparacion = 10;
      display.stopAnimation();
      display.clear();
      beepcount = 0;
    } else if (myState >= 1) {      //forced stop/reset
      // beep_req=1;
      beeptimestamp = millis();    //timestamp beeper
      triplebeeptimestamp = millis();
      Serial.println("Abort/reset");
      display.clear();
      myState = triple_beep;
    }
  }

  switch (myState) {

    case standby:
      //display scroll text, enviar por BT
      if (!display.Animate()) {
        display.startStringScroll_P(FlashString, 250);
      }
      break;

    case comienzo_10:
      // timer de 10 segundos preparacion inicial
      if (TimePeriodIsOver(waiting10_timer, 1000)) {
        // tick cada 1 segundo
        dbg("Tiempo restante preparación ", tiempo_preparacion);
        display.showString("PR", 2, 0);
        display.showNumber(tiempo_preparacion, false, 2, 2);
        tiempo_preparacion--;
      }
      if (tiempo_preparacion == 0) {
        display.showNumber(tiempo_preparacion, false, 2, 2);
        beep_req = true;                //pedir beep
        beeptimestamp = millis();       //timestamp beeper
        waiting180_timer = millis();    //timestamp turno A
        tiempo_turno = duracion_turno;   //init duración turno
        //display.clear();
        myState = turno_a;
      }
      break;

    case turno_a:
      // ciclo turno A
      if (TimePeriodIsOver(waiting180_timer, 1000)) {
        // tick cada 1 segundo
        dbg("Tiempo restante A ", tiempo_turno);
        display.showString("A", 1, 0);
        display.showNumber(tiempo_turno, false, 3, 1);
        tiempo_turno--;
      }
      if (tiempo_turno == 0) {
        beep_req = true;
        tiempo_preparacion = 10;     //init duración intermedio
        beeptimestamp = millis();    //timestamp beeper
        waiting10_timer = millis();  //timestamp para inicio de preparación
        //display.clear();
        myState = intermedio_10;
      }
      break;

    case intermedio_10:
      if (TimePeriodIsOver(waiting10_timer, 1000)) {
        // tick cada 1 segundo
        dbg("Tiempo restante intermedio ", tiempo_preparacion);
        display.showString("PR", 2, 0);
        display.showNumber(tiempo_preparacion, false, 2, 2);
        tiempo_preparacion--;
      }
      if (tiempo_preparacion == 0) {
        display.showNumber(tiempo_preparacion, false, 2, 2);
        beep_req = true;
        beeptimestamp = millis();       //timestamp beeper
        waiting180_timer = millis();    //timestamp turno A
        tiempo_turno = duracion_turno;   //init duración turno
        //display.clear();
        myState = turno_b;
      }
      break;

    case turno_b:
      // timer de 180 segundos
      if (TimePeriodIsOver(waiting180_timer, 1000)) {
        // tick cada 1 segundo
        dbg("Tiempo restante B ", tiempo_turno);
        display.showString("B", 1, 0);
        display.showNumber(tiempo_turno, false, 3, 1);
        tiempo_turno--;
      }
      if (tiempo_turno == 0) {
        // beep_req = true;
        beeptimestamp = millis();    //timestamp beeper
        triplebeeptimestamp = millis();
        display.clear();
        myState = triple_beep;
      }
      break;

    case triple_beep:
      if (!beeping && TimePeriodIsOver(triplebeeptimestamp, 1000)) {
        beeptimestamp = millis();
        beep_req = true;

        dbg("conteo beep", beepcount)
        dbg("triple beep_req end", millis());
        beepcount++;
        if (beepcount >= 3) {
          myState = standby;
          break;
        }
      }

      break;  // end
  }
  beeperv2();   //funcion recurrente beeper
}


// easy to use helper-function for non-blocking timing
boolean TimePeriodIsOver(unsigned long &startOfPeriod, unsigned long TimePeriod) {
  unsigned long currentMillis = millis();
  if (currentMillis - startOfPeriod >= TimePeriod) {
    // more time than TimePeriod has elapsed since last time if-condition was true
    startOfPeriod = currentMillis;  // a new period starts right here so set new starttime
    return true;
  } else return false;  // actual TimePeriod is NOT yet over
}

// beeper function, 1s on
void beeperv2() {
  if (beep_req == true) {
    digitalWrite(LED_BUILTIN, HIGH);
    beeping = true;
    //tone(8, 400, 3000);
    dbgi("beep_req start", beeptimestamp, 100);
    beep_req = false;
  } else if (TimePeriodIsOver(beeptimestamp, 1000)) {
    //dbgi("beep_req end",millis(),100);
    digitalWrite(LED_BUILTIN, LOW);
    beeping = false;
  }
}

Here's the wokwi link(the code might differ as i'm doing the tests moving the countdown line in it): ERTIMER - Wokwi ESP32, STM32, Arduino Simulator

your code does exactly what you have coded

if you decrement the counter prior to sending it to the display initialise with 11 instead of 10

10 - 1 = 9
11 - 1 = 10

put the if-condition

if (tiempo_turno == 0) {

inside the if-condition that is checking if a second is over

the variable "beeping" can be eliminated.

As a general hint:
if testing the code does not solve a problem or you are still asking why does it not work

  • make more variables visible by printing to the serial monitor
  • take a sheet of paper and do a step by step simulation on the paper with writing down how each variables and each condition evaluates
  • add a single-step-button which makes the code stop until you press the button
1 Like

Hello Eliminateur

I am, as always, very curious.

For which game is this interesting countdown used?

1 Like

It's for archery, we plan to put this in a neat box(we still have to decide on the power supply and the buzzer type if it's active or passive, maybe 2 buzzers) for use in the archery club for practice days as these are the official times for olympic archery tournaments and in busy days with lots of people the shooting line can get messy without timing.

You have 10s to walk to the shooting line and set up your position, then the 180s to shoot, after that the 2nd group walks in in their 10s and repeats for their turn.

I did that modification, it makes sense, thanks.

edit: ¿how come it can be eliminated? (i eliminated the other variable and now i have no pauses between beeps, need to restore it)
edit2: i restored the code and i still don't have multiple beeps anymore at the end but one long one, i have to backtrack to see what did i broke...

To solve the start time not showing all i did was to add the display code to the prior state so it shows the variable before decrementing after the first second passes, instead of adding a second.
I also changed the triple beep for a double as again, the 1st beep is called before the state change.

The next step is to clean the code a little bit more, rearrange stuff so it's as similarly structured as possible, add/clean comments, then wrap and integrate this code with remoteXY code to be able to control this via BLE.
So that if someone is practising for a tournament and it's alone(or few people all shooting), they don't have to walk to the timer to start it then run to the line and can start it/configure it via remotexy app.

When it's finished i'll probably post the entire build somewhere, maybe hackaday(no idea if there are other "project" sites nowadays) to see if it would be of use to other archery clubs.

1 Like

What do you expect if I say eliminate variable "beeping"
and you eliminate a

different variable ????!!!

here is your WOKWI simulation with the modifications.
it beeps 3 times with totally eliminated variable "beeping"

best regards Stefan

1 Like

Stefan,
i did delete the beeping variable, then another one, that's what happens when i type and edit a stream of conscience. :face_with_spiral_eyes:

I checked the simulation you pasted but it has the same problems as mine(the tone command is set to 500mS but it should be set to 1s(i set it to 500 for testing)): look at the onboard led and when it's the 3 beeps turn it just stays lit for 3 seconds as there's no "off" time -same with the sound if you change the tone to 1000mS-).

I just had a inspiration and what i did to "fix" this is to change the triple beep delay to 2s, that way it asks for beep, starts the triple_beep timer, beeps for 1s, then 1s later repeats, the problem was that both the beep and timer timeouts ended at the same time:

    case triple_beep:
      if (TimePeriodIsOver(triplebeeptimestamp, 2000)) {

Which Arduino do you use for your archery project?

You should describe very precise with numbers for everything:
how long shall the beep beeing heard?
how long shall the LED beeing switched on?
how long shall the beeper be silent?
how long shall LED be switched off?

This all can be coded in thousands of different ways
You are working on an informatic project and what is most needed in an informatic project is information.

1 Like

For the project i'm going to use a ESP32-S3 board(practically any of them will do) since the S3 has a much higher power BT radio than the regular ESP32.
The processing is overkill but it's mainly for the integrated BT module.
If i were to use any other atmega arduino(like the pro-mini with the 328P which is more than enough for this project) i'd have to buy a HM-10 BLE module which the module alone is MORE expensive than an entire ESP32 board.

Indeed i should've be more specific on some parts and/or commented the code better.
the onboard led is just a standin to have a representation of the output in the simulator.
Same with the buzzer being activated by the TONE function(wokwi has no active buzzer).

Since at this point i still don't know if we're going to use an active or passive buzzer/horn. If it's passive then it's super easy as essentially the entire beeping code would be reduced to a single tone call with X duration.
If it's active then i can use the code i have now and simply set the relevant/s pin high.

so all in all now it's "finally" working, once i get some hardware in 2+ months(that's the delivery time with ali) i'll be able to make a proper test

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.