Search This Blog

Wednesday 1 November 2017

Event-driven programming with callbacks

The previous article (which you should read now, before you continue) how "callbacks" made programming the ESP8266 easier and less error-prone - but what do they look like and how do they work?

Let's take the case where you want to read a sensor every minute. I've seen a lot of code like this around (or variations of it)

#define SENSOR 5

void setup(){
  Serial.begin(74880);
  pinMode(SENSOR,INPUT);

}

void loop(){
  if (millis()%60000){
    Serial.println("do something");
  }
}
Looks OK? Often the if(millis()... will be taking the current time, subtracting the previous value and checking if it == 1000 which of course requires a global variable for the previous value and some extra code, but the principle is the same - and it doesn't work!

loop() gets called about 40,000 times a second. So you are likely to "do something" up to 40 times because the value of millis() will be the same until another millisecond elapses! So, depending on how long "do something" takes, depends on how often it will be called, which is nothing like what you think you were doing, and if "do something" relies on accurate timing, your program will not work.

"Easy!" you think, "I'll set another global variable while doing something, then check it in loop and make sure I only do something once per loop".

"or, I can put delay(60000) inside the loop and then my timing will be accurate!"

The first option adds more code, more complexity (none of which is necessary and usually frowned upon - for plenty of good reasons - by experienced programmers) and the second just won't work.

No, the solution is to use the Ticker library which runs a highly accurate timer and calls back your code when the timer expires:

#include<Ticker.h>
#define SENSOR 5

Ticker  everyMinute; 

void doSomething(){                        // this is your callback function
  Serial.println("do something");
}

void setup(){
  Serial.begin(74880);
  pinMode(SENSOR,INPUT);
  everyMinute.attach_ms(60000,doSomething); // "register" your callback
}

void loop(){
}

The most important thing to realise here is that doSomething does not get called by everyMinute.attach_ms(60000,doSomething) in setup...all you are doing here is telling the Ticker library the name of your function - "registering" it - which will then be called every minute.

In a nutshell, that's how callbacks work. They are lot simpler, a lot cleaner and prevent you from re-inventing the wheel every time you write a sketch. But the most important  thing, is that they "just work" and they avoid numerous common problems.

Imagine if you had three or four sensors which need reading at different times...the loop code would very soon start to get complicated...using Ticker, you just have three or four tickers going off at different times, each with its own separate (obvious) callback which does just what that sensor needs. It's a lot more obvious and easier to read as well as being a lot less error-prone.

If you use "lambda" functions (and if you don't, you should - google them now) it's  even easier:

#include<Ticker.h>
#define SENSOR 5

Ticker  everyMinute;  

void setup(){
  Serial.begin(74880);
  pinMode(SENSOR,INPUT);
  everyMinute.attach_ms(60000,[](){ Serial.println("do something"); });
}

void loop(){
}

The "callback" is defined "inline" with the thing that will call it and saves having a separately defined function.

The Ticker library also allows you to pass a single (32-bit) parameter to your callback function, which is extremely useful and solves a lot of additional issues in the majority of cases. If however you want to pass two parameters, or call a class method when the timer "fires" - you are in for a lot of "fun" - unless you look at the author's "H4" library github.com/philbowles/h4 which is specifically designed to do just those things. It also adds more creative timer functions, such as calling back at random times or calling back a fixed number of times. Finally, it allows you to "chain" functions, i.e. call one after another has just finished. This allows some quite complex sequences to be built in to your code very simply indeed.

If the H4 library is used correctly, you will never need to call delay()...nor will ever need to know (far less need to muck about with) the "watchdog timer" and if you don't yet know why those are good things, read the next two articles!

It also does something much more important to prevent common errors, but I'll explain that later, once you are more familiar with this new "event-driven" style.

No comments:

Post a Comment