Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: Option to trigger repeating timer immediately #57

Open
nickion opened this issue Sep 8, 2021 · 10 comments
Open

Suggestion: Option to trigger repeating timer immediately #57

nickion opened this issue Sep 8, 2021 · 10 comments

Comments

@nickion
Copy link

nickion commented Sep 8, 2021

It would be useful to have a signature that specified whether a timer should start already expired so that it fires on the first timer tick via loop() rather than only after the specified interval. In this way an initial action can be performed without the need to explicitly trigger the callbacks.

@philj404
Copy link
Contributor

philj404 commented Sep 9, 2021

Instead of complicating the interface, would it be just as easy to call the task directly the first time (as soon as the processor is initialized enough to handle it)? For example:

...
    timer.every(myInterval, myTask); // run myTask periodically
    myPeriodicTask(); // ... and run it NOW because it has to start early
...

@nickion
Copy link
Author

nickion commented Sep 10, 2021

Instead of complicating the interface, would it be just as easy to call the task directly the first time (as soon as the processor is initialized enough to handle it)? For example:

It's a workaround but is somewhat clumsy, and in some cases other work might also need to be duplicated in setup() or the timer action might need to be triggered the first time in loop(), with an associated boolean, to ensure correct ordering of operations. Simply having support for both leading and falling edge triggers would be more expressive, simplifying application code, reducing possible bugs (such as accidental deletion of the initial trigger), and keeping the timer callback triggers exclusively the responsibility of the timer infrastructure.

There is also another take on this which is to recognise that having a timer expire either immediately or after a full cycle are just special cases of the more general concept of allowing the starting timer value to be specified. If the timer start point can be specified, a timer could expire first after say 5 seconds and thereafter every 60 seconds, which could also be nice in some cases.

@bato3
Copy link

bato3 commented Jul 6, 2022

I would like to remind you that most languages have the do-while structure.

@skrobinson
Copy link
Contributor

@nickion,

Your original suggestion was for an immediate (on first tick) and repeating triggers. What about scheduling myTask to be called once on the next tick and every interval ticks?

timer.in(0, myTask); // run myTask once on next tick
timer.every(myInterval, myTask); // run myTask periodically

@nickion
Copy link
Author

nickion commented Jul 7, 2022

@nickion,

Your original suggestion was for an immediate (on first tick) and repeating triggers. What about scheduling myTask to be called once on the next tick and every interval ticks?

Thanks, and yes that can be done as a compromise. It would be more expressive and neater though to have the right tool for the job, Kind of like a manufacturer who makes push buttons that only activate on release, and to someone who wants one that activates on press, saying to just press it really quickly; it might work, but would simply be better to have one that behaved as needed.

@philj404
Copy link
Contributor

philj404 commented Jul 8, 2022

I like @skrobinson's suggestion.

Here is a helper function which has the requested feature:

Timer<>::Task timer_now_and_every(unsigned long myInterval, Timer<>::handler_t myTask)
{
    timer.in(0, myTask); // run myTask once on next tick
    return timer.every(myInterval, myTask); // run myTask periodically
}

While it works, I do not like the function name I chose. It is not nearly as intuitive/elegant as the built-in method names timer.in(...), timer.at(...) and timer.every(...).

Since choosing a good name is important for a good library, I wouldn't want to make clients use this hack as it is. (It's OK for personal use, as there are fewer places to change once I find a better name.)

If you would like to experiment with a "show and tell" example and perhaps find a better function name, here's my sketch:

// Sketch to demonstrate helper function timer_now_and_every()
//
// It helps timer.every() start its task with no delay
//
#include <arduino-timer.h>

auto timer = timer_create_default();

////////////////////////////////////////////////////////////
Timer<>::Task timer_now_and_every(unsigned long myInterval, Timer<>::handler_t myTask)
{
    timer.in(0, myTask); // run myTask once on next tick
    return timer.every(myInterval, myTask); // run myTask periodically
}
////////////////////////////////////////////////////////////

bool show_elapsed_time(void *) {
  Serial.print(F("Running at: "));
  Serial.print(millis()/1000.);
  Serial.println(F(" seconds."));
  
  return true; // repeat? true
}

void setup() {
  Serial.begin(115200);
  Serial.println(F( "\nRunning " __FILE__ ", Built " __DATE__));

  timer_now_and_every(5000, show_elapsed_time); // show time IMMEDIATELY
}

void loop() {
  timer.tick(); // tick the timer
}

@nickion
Copy link
Author

nickion commented Jul 9, 2022

Good suggestion. Thinking about it more this morning, the more general form would be a fire_and_repeat operation, with both the interval before the first firing specifiable (i.e. a delay), the repeat interval and a count; instant firing would then be a special case of the more general form. Given that, you have possibilities such as:

Fire immediately:
fire_and_repeat(NOW, REPEAT_NEVER)

Fire once after/in 10 units:
...(10, REPEAT_NEVER) or ...(10, 0, 0)

Fire every 10 units until cancelled:
...(10, 10, CONTINUOUS)

Fire after 1 unit and then every 10 units up to 5 times:
...(1, 10, 5)

etc.

Convenience methods such as firing once after a delay, repeated firing every number of units etc. are specialisations.

@skrobinson
Copy link
Contributor

Fire immediately:
Fire once after/in 10 units:
Fire every 10 units until cancelled:

These are synonyms for in(0, myTask), in(10, myTask), and every(10, myTask). Your examples shift the naming distinction from the function name to a function argument. Is this an improvement?

Fire after 1 unit and then every 10 units up to 5 times:

Would this call the function at 1, 10, 20, 30, 40 or 1, 11, 21, 31, 41? Why is that choice preferred?

@philj404,

On a meta-related note... Would a Recipes Discussions category be useful? It could contain ways to use the existing API for these (and other) scenarios. A good Recipes first entry would be timer_now_and_every.

@nickion
Copy link
Author

nickion commented Jul 11, 2022

I'd implement every 10 seconds with the first after 1 second as 1,10,20,, as that is the more likely use case, but 1, 11, 21 might be useful too so having that as an option could have merit.

The thing to recognise is that there's fundamentally only one type of timer, and what we might think of as different types of timers can all be expressed in terms of the generic timer.

@philj404
Copy link
Contributor

@skrobinson I just added my example to Show and Tell discussion #67 . That's pretty similar to your Recipes category idea -- and we're not using the Show and Tell yet.

@nickion I agree I would (personally) prefer to just use and maintain the in()/at()/every() primitives. I have trouble remembering argument order once there is more than one value 😄 . But people with larger brains can handle it.

Sometimes interfaces must get more complicated than basic primitives. I'm not sure that is the case here (maybe this use case is too specific for general interest). I would rather avoid growing the interface if possible, as doing so creates more testing and support issues.

Regardless, sometimes a developer needs something more complicated than what a library provides. It keeps development interesting. Helper functions (like this) can help you make a simple library more powerful, even when they are not built in.

If it turns out that many/most clients are using a helper, then maybe it is important enough it should be added to the base library. In the meantime others can add the example helper to their code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants