arkhipenko / TaskScheduler

Cooperative multitasking for Arduino, ESPx, STM32, nRF and other microcontrollers
http://playground.arduino.cc/Code/TaskScheduler
BSD 3-Clause "New" or "Revised" License
1.23k stars 224 forks source link

Reset a task start time #41

Closed AcuarioCat closed 6 years ago

AcuarioCat commented 6 years ago

Hi, is it possible to reset a task start time while the task is active? This is my scenario: I want a task to control the backlight on an lcd so the display turns off when there is no user activity for say 30 seconds. I would like to start a task for this (no problem) but if there is some activity from a user then I want to reset the task execution start time back to the beginning to delay the task. I guess I can do this with disable/enable but is there another cleaner way?

arkhipenko commented 6 years ago

I use restartDelayed() for this purpose all the time.

The task you are talking about is a Timeout task. Its callback method implements timeout functions (e.g. turn the display off, put device to sleep, etc.). Any other task, that should prevent a timeout should call Timeout.restartDelayed(); in their callback method.

Please check my pumpkin project. There is a timeout task implemented there.

Cheers, Anatoli

Sent from a mobile device. Apologies for accidental typos.

On Dec 24, 2017 10:58 AM, AcuarioCat notifications@github.com wrote:

Hi, is it possible to reset a task start time while the task is active? This is my scenario: I want a task to control the backlight on an lcd so the display turns off when there is no user activity for say 30 seconds. I would like to start a task for this (no problem) but if there is some activity from a user then I want to reset the task execution start time back to the beginning to delay the task. I guess I can do this with disable/enable but is there another cleaner way?

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHubhttps://github.com/arkhipenko/TaskScheduler/issues/41, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AATGTRDowsC9gczzbLDmtRdwwRRn12txks5tDiBWgaJpZM4RL4oh.

arkhipenko commented 6 years ago

Make sure you enableDelayed() your timeout task at the very end of setup() method. Otherwise it will trigger immediately...

arkhipenko commented 6 years ago

Make sure you enableDelayed() your timeout task at the very end of setup() method. Otherwise it will trigger immediately...

mm108 commented 5 years ago

hi, a small doubt related to the same issue - If I were calling <taskname>.restartDelayed(); without passing the parameter(unsigned long aDelay), I might as well straightaway call <taskname>.resetTimeout();?

Here's something about what I am trying to achieve: When a certain thing happens, I want one of the tasks to start counting from the beginning again (say the task is set to run at 3 Sec interval and 2 Secs have since passed, it should reset back to 3 Sec again).

I was thinking of the best or recommended way to do that. I read that you mentioned restartDelayed() but then I came across resetTimeout() in the documentation and thought I should check. Thanks a ton.

Cheers

mm108 commented 5 years ago

Ok, I think I understand this. resetTimeout() just resets the interval back to what was originally set and starts the (time) count from start again.

restartDelayed() on the other hand gives and extra parameter which makes it work in this manner. An example of restartDelayed(n) called with the parameter n

  1. The task gets stopped if it was running
  2. There will be delay of n seconds.
  3. After the completion of n seconds, the task is enabled again and starts executing with whatever interval was set previously.

... unless I got any of the above wrong

Cheers

arkhipenko commented 5 years ago

Fundamentally the restart() and restartDelayed() methods are for regular execution. In your case restartDelayed() should do the trick.

An example of a user story for this type of functionality could be: "Restart blinking an LED 2 times per second indefinitely when a button is pressed".

resetTimeout() is part of the overall timeout functionality which disables the task completely regardless of the remaining iterations, time left, etc...

resetTimeout() just restarts overall task timeout counter, but does not affect current task execution, so if task was on iteration 10 and 1 second from next callback (say every 3 seconds), it will continue to wait for 1 second and then trigger iteration 11.

restartDelayed() alternatively will reset iteration counter to 1 and trigger callback in 3 seconds as if it was first time.

An example of a user story for this type of functionality could be: "Stop blinking LED completely after 5 seconds regardless of which iteration it is or how much time is left until next iteration".

There is a subtle difference between use cases. enable and restart methods are for functional purposes, while timeout is more for exceptions.

Does it make sense?

mm108 commented 5 years ago

Cool, it makes perfect sense. And I understand the difference now. Thanks a lot.

mm108 commented 5 years ago

Oops, when I execute the restartDelayed() 3-4 times, the ESP32 reboots :-/ I am trying to figure out why but the reboot's never happened before until I made the change to reset the task interval.

I am thinking I could disable => enable the task. I guess that resets the interval ( start counting from zero again?) Thanks

Cheers

arkhipenko commented 5 years ago

I never tested TaskScheduler on ESP32 myself. However I find it very strange that a restart method would cause a reset. Are you sure it is not something else ?

On Dec 25, 2018 10:10 AM, RW notifications@github.com wrote:

Oops, when I execute the restartDelayed() 3-4 times, the ESP32 reboots :-/ I am trying to figure out why but the reboot's never happened before until I made the change to reset the task interval.

— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHubhttps://github.com/arkhipenko/TaskScheduler/issues/41#issuecomment-449856875, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AATGTdptDvucMVW28nnsxm_PMho3MrxDks5u8j_bgaJpZM4RL4oh.

mm108 commented 5 years ago

hi, yes I can confirm that the restartDelayed() has something to do with the reboots. I commented out the part where I reset the timeout and then the reboots don't happen anymore. Thanks

Cheers

arkhipenko commented 5 years ago

Could you please send your full sketch to arkhipenko@hotmail.com ?

On Dec 25, 2018 10:56 AM, RW notifications@github.com wrote:

hi, yes I can confirm that the restartDelayed() has something to do with the reboots. I commented out the part where I reset the timeout and then the reboots don't happen anymore. Thanks

Cheers

— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHubhttps://github.com/arkhipenko/TaskScheduler/issues/41#issuecomment-449859894, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AATGTfr5ulCvvHtsDFjNCFYy4EKwTf3aks5u8krDgaJpZM4RL4oh.

mm108 commented 5 years ago

hi, yes but I'll probably need to comment out a number of lines because the sketch is huge and has a lot of lines dealing with other hardware, sensors, OLED screen etc. I guess it'll would throw a whole bunch of errors if run without the same hardware connected to the same pins :-/ Cheers

arkhipenko commented 5 years ago

That's ok. You can send as is, or please make sure it still resets after you comment out the lines.

On Dec 25, 2018 12:02 PM, RW notifications@github.com wrote:

hi, yes but I'll probably need to comment out a number of lines because the sketch is huge and has a lot of lines dealing with other hardware, sensors, OLED screen etc. I guess it'll would throw a whole bunch of errors if run without the same hardware connected to the same pins :-/ Cheers

— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHubhttps://github.com/arkhipenko/TaskScheduler/issues/41#issuecomment-449863288, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AATGTSDo4J39wFfM_AABgpeWWxhOern5ks5u8lo0gaJpZM4RL4oh.

mm108 commented 5 years ago

When I was commenting out some lines I accidentally commented out couple of lines that have to do with ESPAsyncWebServer. After that the reboots haven't happened so far. Also the restartDelayed() when run in a separate sketch many times over does not cause a reboot. So I guess it could be some conflict between the 2 libraries - or the combo being heavy on the memory or something. I'll send the sketch after commenting out some lines - there is a whole lot of lines just related to OLED display and sensors scattered in the sketch that won't run on general esp32 dev boards. I'll be back with an update. Still so far one of the best and comprehensive libraries I have come across, and also great support. Thanks @arkhipenko once again for that. Cheers.

mm108 commented 5 years ago

hi, I think I may have found the reason for reboot. What I was trying to do was this - In the scheduled task callback I was incrementing a global variable (if it's value was lower than a certain value). And in the onEnable callback method, I was resetting the value of the global variable to zero (if the value happened to be above 0). I think under certain situation these 2 happened precisely at the same time resulting in the ESP32 crashing/rebooting. Now I slightly modified the code to do this

if (stModeNotify.isEnabled()) {
    // Disable the task
    stModeNotify.disable();
    // Delete the task
    runner.deleteTask(stModeNotify);
    // If the counter is greater than 0, then reset it to 0
    if (idleCounter > 0) {
          idleCounter = 0;
          Serial.println("idleCounter reset to 0!");
    }
    // Add the task again
    runner.addTask(stModeNotify);
    // Enable the task
    stModeNotify.enable();
}

The above is lengthier but it has never crashed once.

The original sketch is something similar to the below sketch in terms of setting, resetting the global variable. The below sketch hasn't crashed but I think that is because the timing (of setting the variable) didn't conflict yet (those few times I tried).

#include "WiFi.h"
#include "ESPAsyncWebServer.h"
String SoftAPName = "ESP32AP";
String SoftAPPassword = "12345678";

/* TaskScheduler */
#include <TaskScheduler.h>
AsyncWebServer server(80);

/* ** Callbacks methods prototypes ** */
bool idleTimerReset();

/* ** Notify of Config Mode ** */
void stCBModeNotify();
Task stModeNotify(30000, TASK_FOREVER, &stCBModeNotify, NULL, false, &idleTimerReset);

Scheduler runner;
byte idleCounter = 0;
const byte MAX_IDLECOUNTER = 5;

void disableSTModeNotify() {
  stModeNotify.disable();
  runner.deleteTask(stModeNotify);
}

void stCBModeNotify() {
  Serial.println(F("Task run:: stCBModeNotify"));

  if (idleCounter >= MAX_IDLECOUNTER) {
    Serial.println(F("\"idleCounter\" reached max. Disabling task \"stModeNotify\""));
    disableSTModeNotify();
  } else {
    idleCounter = idleCounter + 1;
    Serial.println(F("\"stCBModeNotify\" called!"));
  }
}

bool idleTimerReset() {
  if (idleCounter > 0) {
    idleCounter = 0;
    Serial.println(F("idleCounter reset to 0!"));
  }
  return true; // Task should be enabled
}

void setup() {
  Serial.begin(115200);

  WiFi.mode(WIFI_AP); //
  while (!WiFi.softAP(SoftAPName.c_str(), SoftAPPassword.c_str())) {
    Serial.print(".");
  }
  Serial.println("");
  Serial.print(F("Access Point IPv4: "));
  Serial.println(WiFi.softAPIP());

  runner.init();
  runner.addTask(stModeNotify);
  stModeNotify.enable();

  server.on("/", HTTP_GET, [](AsyncWebServerRequest * request) {
    /* Every request we are resetting the counter (in the onEnable callback) */
    if (stModeNotify.isEnabled()) {
      stModeNotify.restartDelayed();
    }
    request->send(200, "text/plain", "Hello World!");
  });
  server.begin();
}

void loop() {
  runner.execute();
}

Thanks a ton @arkhipenko for your help.

arkhipenko commented 5 years ago

The only reason I can think of right now is this:

runner.deleteTask(stModeNotify)

Could you try commenting this out?

You don't really need to delete the task from the chain, and in your case this is the only task on the chain, so when you delete it there is a bunch of null pointers which I may accidentally call some causing a reset. I don't think I have thoroughly tested a case of an empty task chain for a simple reason that such chain does not make sense to have...

Also, you could limit the number of iterations of stModeNotify to MAX_IDLETIMES instead of forever and handle Max idletime situation in the OnDisbale method instead of having a separate global variable.

Hope this helps.

On Dec 26, 2018 6:46 AM, RW notifications@github.com wrote:

hi, I think I may have found the reason for reboot. What I was trying to do was this - In the scheduled task callback I was incrementing a global variable (if it's value was lower than a certain value). And in the onEnable callback method, I was resetting the value of the global variable to zero (if the value happened to be above 0). I think under certain situation these 2 happened precisely at the same time resulting in the ESP32 crashing/rebooting. Now I slightly modified the code to do this

if (stModeNotify.isEnabled()) { // Disable the task stModeNotify.disable(); // Delete the task runner.deleteTask(stModeNotify); // If the counter is greater than 0, then reset it to 0 if (idleCounter > 0) { idleCounter = 0; Serial.println("idleCounter reset to 0!"); } // Add the task again runner.addTask(stModeNotify); // Enable the task stModeNotify.enable(); }

The above is lengthier but it has never crashed once.

The original sketch is something similar to the below sketch in terms of setting, resetting the global variable. The below sketch hasn't crashed but I think that is because the timing (of setting the variable) didn't conflict yet (those few times I tried).

include "WiFi.h"

include "ESPAsyncWebServer.h"

String SoftAPName = "ESP32AP"; String SoftAPPassword = "12345678";

/ TaskScheduler /

include

AsyncWebServer server(80);

/ Callbacks methods prototypes / bool idleTimerReset();

/ Notify of Config Mode / void stCBModeNotify(); Task stModeNotify(30000, TASK_FOREVER, &stCBModeNotify, NULL, false, &idleTimerReset);

Scheduler runner; byte idleCounter = 0; const byte MAX_IDLECOUNTER = 5;

void disableSTModeNotify() { stModeNotify.disable(); runner.deleteTask(stModeNotify); }

void stCBModeNotify() { Serial.println(F("Task run:: stCBModeNotify"));

if (idleCounter >= MAX_IDLECOUNTER) { Serial.println(F("\"idleCounter\" reached max. Disabling task \"stModeNotify\"")); disableSTModeNotify(); } else { idleCounter = idleCounter + 1; Serial.println(F("\"stCBModeNotify\" called!")); } }

bool idleTimerReset() { if (idleCounter > 0) { idleCounter = 0; Serial.println(F("idleCounter reset to 0!")); } return true; // Task should be enabled }

void setup() { Serial.begin(115200);

WiFi.mode(WIFI_AP); // while (!WiFi.softAP(SoftAPName.c_str(), SoftAPPassword.c_str())) { Serial.print("."); } Serial.println(""); Serial.print(F("Access Point IPv4: ")); Serial.println(WiFi.softAPIP());

runner.init(); runner.addTask(stModeNotify); stModeNotify.enable();

server.on("/", HTTP_GET, [](AsyncWebServerRequest request) { / Every request we are resetting the counter (in the onEnable callback) */ if (stModeNotify.isEnabled()) { stModeNotify.restartDelayed(); } request->send(200, "text/plain", "Hello World!"); }); server.begin(); }

void loop() { runner.execute(); }

Thanks a ton @arkhipenkohttps://github.com/arkhipenko for your help.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHubhttps://github.com/arkhipenko/TaskScheduler/issues/41#issuecomment-449955102, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AATGTe14Gh6YawltW3GKUD1Ptiq-8qa0ks5u82GsgaJpZM4RL4oh.

mm108 commented 5 years ago

runner.deleteTask(stModeNotify)

Could you try commenting this out?

Thanks for your input. Yes, I think the runner.deleteTask(stModeNotify); was definitely an overkill! I put it there earlier when I wasn't sure whether the task would start from the first iteration if I just did a disable + enable of the task.

You don't really need to delete the task from the chain, and in your case this is the only task on the chain, so when you delete it there is a bunch of null pointers which I may accidentally call some causing a reset. I don't think I have thoroughly tested a case of an empty task chain for a simple reason that such chain does not make sense to have...

The reason runner.deleteTask(stModeNotify); may have worked without issues in my code could be because there are couple of other tasks in the chain. The minimal sketch that I posted earlier doesn't show those. So now I have commented runner.deleteTask(stModeNotify); out.

Also, you could limit the number of iterations of stModeNotify to MAX_IDLETIMES instead of forever and handle Max idletime situation in the OnDisbale method instead of having a separate global variable.

The above is definitely a better idea. As matter of fact I could even check for isLastIteration() I guess .In my initial sketches I was altering the global variable under certain circumstances. Also now I no longer have that requirement. By using MAX_IDLETIMES as you mentioned, the code will be neater (a few lines less too). Appreciate your input on that. Thanks.

So a big thanks for your answers and time. I think I have gathered enough to fix the problem once for all.

Cheers

mm108 commented 5 years ago

Whoops! I think I may have stumbled on something here. If I execute these 2 methods fast enough (by sending(refreshing) web requests fast enough using the browser or postman), I can get the ESP32 to reboot.

stModeNotify.disable();
stModeNotify.enable();

I tried adding a delay of up to 10 ms but that still causes the reboot but not as quickly as the one without the delay.

stModeNotify.disable();
delay(10);
stModeNotify.enable();

The above code with the delay crashes too but much less frequently than the one without the delay.

arkhipenko commented 5 years ago

I think what might be happening is this:

You made your task part of the web server code http service request routine. I noticed that you disable the task but not the service routine. As far as I know ESP32 has 2 cores: one for user sketches and one for system routines. Since task objects are not volitile, the status of the task is not propagated fast enough leading to all sorts of race conditions and pointer inconsistencies. As I said, I have never tested on ESP32 especially under stress conditions, and two multitasking cores are not exactle cooperative multitasking. Could you try making sure you disable webserver service routine together with the timeout task (before it actually?) Is there any between threads synchronization mechanism for esp32?

On Dec 26, 2018 11:13 AM, RW notifications@github.com wrote:

Whoops! I think I may have stumbled on something here. If I execute these 2 methods fast enough (by sending(refreshing) web requests fast enough using the browser or postman), I can get the ESP32 to reboot.

stModeNotify.disable(); stModeNotify.enable();

I tried adding a delay of up to 10 ms but that still causes the reboot but not as quickly as the one without the delay.

stModeNotify.disable(); delay(10); stModeNotify.enable();

The above code with the delay crashes too but much less frequently than the one without the delay.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHubhttps://github.com/arkhipenko/TaskScheduler/issues/41#issuecomment-449987877, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AATGTb4Z6P-BIdSdHGkX_x5e5odNm7CDks5u86A8gaJpZM4RL4oh.

mm108 commented 5 years ago

You made your task part of the web server code http service request routine. I noticed that you disable the task but not the service routine.

Could you try making sure you disable webserver service routine together with the timeout task (before it actually?)

hi @arkhipenko I am not sure if there is a way I could disable/enable the service but I could do the next best thing - that is to check for interval between the delays. If the delays are too short I could just send a "previous command still processing" or some such message - perhaps a standard http status code/message like 202, 302 or even 303 that directs to some kinda status page/link.

Is there any between threads synchronization mechanism for esp32?

I will need to research the above a little more before I arrive at some conclusion. I am a little new to ESP32 too. But as I need to get a prototype out soon, I will try the faster fix now which is to do some kinda rate limiting - preferably server/device side. I have absolute control (on request timing) over the client side too because I coded that stuff but I think I'll still implement rate limiting on the esp32 side.

I could even "spin" another one-time task whose job is just to reset the original task. If another request comes in (too quickly) when the one-time task is still enabled, the one-time task doesn't get called/executed.

Cheers

arkhipenko commented 5 years ago

I did some reading on ESP32 and it definitely seems like a race condition. The "Protocol" code is running on Core 0 (and that is where the HTTP server code is executed I believe), while all Arduino Tasks are running on Core 1 (Application core). Without explicit coordination between cores TaskScheduler is no longer act as a cooperative multitasking scheduler. I would recommend against running TaskScheduler on ESP 32 until I figure out an easy way to solve this.

mm108 commented 5 years ago

Cool, great find. I'll find some workaround till this is figured out. Thanks.

pulento commented 5 years ago

Wouldn't be possible to just use Core 0 and disable Core 1 on the ESP32 ? It seems that at least on FreeRTOS is possible:

https://www.esp32.com/viewtopic.php?t=4214

arkhipenko commented 5 years ago

Wouldn't be possible to just use Core 0 and disable Core 1 on the ESP32 ? It seems that at least on FreeRTOS is possible:

https://www.esp32.com/viewtopic.php?t=4214

Yes, but it is such a shame to kill the very unique advantage of the board!