Function to set callback, second function to call callback

Hi Everyone,

So, here's what I'm trying to do.

Basic description: I have a class, within that class I want a public function to somehow set a reference to another function into a variable (for callback) and then have a second private function that then calls that function that was set into the variable if it's not null, otherwise run some internal code.

More details: I have an ESP32, which has ESP-NOW. Here is the typical definition of the receive callback.

void OnDataRecv(const esp_now_recv_info_t *esp_now_info, const uint8_t *data, int data_len) {
    // Code to execute within function
}

I'm writing a library which will have the ESP-NOW setup within it if the configuration option "ESP-NOW Enabled" is set to true.

In that library, which is written as a class, I'd like the end user to be able to call something like the following to set their custom callback.

Library.setESPNOWrcvCB(custom_callback);

Than the second function would have something within it like

if (custom_callback) {
    custom_callback();
} else {
    // run preset internal code
}

I know there needs to be more to it than that, but I can't seem to find the right search terms to find any information on doing something like this.

And, obviously the above code is not real code, but just to give the concept of what I'd like to achieve.

If anyone has any links to info on how to do something like this, search terms that might be helpful for google or even an example, I'd appreciate any help I can get. Thank you!

How about looking into "pointers to functions" ?

look this over

output

run
myFunc
void myFunc () {
    Serial.println (__func__);
}

// -----------------------------------------------------------------------------
class Something {
    void (*callback) (void);

  public:
    Something (void) { }
    Something (void f ()) { callback = f; }

    void run ()  {
        if (callback)
            callback ();
        else
            Serial.println (__func__);
    }
};

Something one;
Something two (myFunc);

// -----------------------------------------------------------------------------
void
setup (void)
{
    Serial.begin (9600);

    one.run ();
    two.run ();
}

void loop () { }

I have already done this, but it doesn't exactly explain, or give examples of what I'm looking to do. Or maybe I'm just not seeing it. What I was looking for was examples of exactly what I'm trying to do, not just generic information about pointers to functions. While that info may be helpful to start with, I need more assistance than that. But I do appreciate the starting point (which I forgot to mention I had already looked at).

This was very helpful, but missing arguments and I kind of wanted a function under the class to set the callback function rather than having an additional parameter while instantiating the class. I see how that can be useful, but in my case the ESP-NOW may or may not be enabled based on an option the user sets in a web based configuration. Here's what I was able to figure out between that and a little more searching. Let me know if anything looks wrong, or rather if anything looks like it should/could be done a different/better way. Keep in mind this does not include any code to instantiate the class or to check (upon instantiating the class) if ESP-NOW is enabled or not. This is just the part of the code related to the two functions for setting the callback and calling it. I still have to write much of the code, but adding these parts seemed to compile okay, which doesn't necessarily mean it's the best way to do it.

Library.h

class MyClass {
private:
	void (*NOWrcvCB)(const esp_now_recv_info_t*, const uint8_t*, int) = nullptr;
	void (*NOWsntCB)(const uint8_t*, esp_now_send_status_t) = nullptr;
	// This was just for testing, but I might just set to a default function instead
	// of using nullptr and setting a function later if one isn't user defined.
public:
	void SetNOWrcvCB(void (*rcvcb)(const esp_now_recv_info_t*, const uint8_t*, int));
	void SetNOWsntCB(void (*sntcb)(const uint8_t*, esp_now_send_status_t));
	void Begin();
	void Loop();
}

Library.cpp

void MyClass::SetNOWrcvCB(void (*rcvcb)(const esp_now_recv_info_t*, const uint8_t*, int)) {
	this->NOWrcvCB = rcvcb;
}
void MyClass::SetNOWsntCB(void (*sntcb)(const uint8_t*, esp_now_send_status_t)) {
	this->NOWsntCB = sntcb;
}
void MyClass::Begin() {
	// Code will go here to actually check if ESP-NOW is enabled in the config and start it up
}
void MyClass::Loop() {
	// Code to potentially run at loop time
}

MainProgram.ino

MyClass abc;
void OnDataRecv(const esp_now_recv_info_t *esp_now_info, const uint8_t *data, int data_len) {
	// Just a basic function to print incoming data.  I used this to help decipher a remote that I bought that uses ESP-NOW.
	char mac[18];
	snprintf(mac, sizeof(mac), "%02x:%02x:%02x:%02x:%02x:%02x", esp_now_info->src_addr[0],
			esp_now_info->src_addr[1], esp_now_info->src_addr[2], esp_now_info->src_addr[3],
			esp_now_info->src_addr[4], esp_now_info->src_addr[5]);  // Put MAC into a string
	Serial.printf("MAC Address: %s\n", mac);
	Serial.printf("Data Length: %d\n", data_len);
	for (size_t i = 0; i < data_len; ++i) {
		Serial.printf("data[%d] = %d\n", i, data[i]);  // Print each bit, could also print the data as a string if needed
	}
}
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
	// Nothing here yet, just showing the definition of the function
}
void setup() {
	abc.SetNOWrcvCB(OnDataRecv);
	abc.SetNOWsntCB(OnDataSent);
	abc.Begin();
}
void loop() {
	abc.loop();
}

EDIT: I'm having a hard time figuring out exactly how to set the default function in the class definitions. Note that I did change the functions to have an underscore in the beginning as the ones in the main program do not and I wanted to have the internal vs user functions to have separate names. I was trying this...

    void _OnDataRecv(const esp_now_recv_info_t *esp_now_info, const uint8_t *data, int data_len);
    void _OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status);
    void (*NOWrcvCB)(const esp_now_recv_info_t*, const uint8_t*, int) = _OnDataRecv;
    void (*NOWsntCB)(const uint8_t*, esp_now_send_status_t) = _OnDataSent;

and got this...

Library.h:99:73: error: cannot convert 'MyClass::_OnDataRecv' from type 'void (MyClass::)(const esp_now_recv_info_t*, const uint8_t*, int)' {aka 'void (MyClass::)(const esp_now_recv_info*, const unsigned char*, int)'} to type 'void (*)(const esp_now_recv_info_t*, const uint8_t*, int)' {aka 'void (*)(const esp_now_recv_info*, const unsigned char*, int)'}
   99 |     void (*NOWrcvCB)(const esp_now_recv_info_t*, const uint8_t*, int) = _OnDataRecv;
      |                                                                         ^~~~~~~~~~~
Library.h:100:63: error: cannot convert 'MyClass::_OnDataSent' from type 'void (MyClass::)(const uint8_t*, esp_now_send_status_t)' {aka 'void (MyClass::)(const unsigned char*, esp_now_send_status_t)'} to type 'void (*)(const uint8_t*, esp_now_send_status_t)' {aka 'void (*)(const unsigned char*, esp_now_send_status_t)'}
  100 |     void (*NOWsntCB)(const uint8_t*, esp_now_send_status_t) = _OnDataSent;
      |                                                               ^~~~~~~~~~~

What am I missing that is right in front of me?

You had the right idea at first, and it looks like @gcjr's example comes down to the same thing.

The default function is called if no callback has been set by the user, either through function signature overloading or setting by another class method:

if (custom_callback) {
    custom_callback();
} else {
    // run preset internal code
}

or same same

        if (callback)
            callback ();
        else
            Serial.println (__func__);

where I guess the printing is proxy for the default action.

As seen in the example, you can write two constructors. So you could provide two constructors as well as a method for setting/changing/removing the callback, eveyone is happy.

a7

You're missing the fact that pointers to member functions are a different type than pointers to free functions (even if they take the same parameters and return the same type). See: https://isocpp.org/wiki/faq/pointers-to-members

You could set the default callbacks to member functions by using std::function:

#include <functional>

class MyClass {
  private:
    using RxCb = std::function<void (const esp_now_recv_info_t*, const uint8_t*, int)>;
    using TxCb = std::function<void (const uint8_t*, esp_now_send_status_t)>;

    // Set default callback
    RxCb NOWrcvCB {[this](const esp_now_recv_info_t* p1, const uint8_t* p2, int i1) {
      this->defaultRxCb(p1, p2, i1);
    }};

    // Set default callback
    TxCb NOWsntCB {[this](const uint8_t* p1, esp_now_send_status_t s1) {
      this->defaultTxCb(p1, s1);
    }};

    void defaultRxCb(const esp_now_recv_info_t*, const uint8_t*, int) {
      // Default Callback. May be overridden by user code
    }

    void defaultTxCb(const uint8_t* p1, esp_now_send_status_t s1) {
      // Default Callback. May be overridden by user code
    }

  public:
    void SetNOWrcvCB(RxCb rcvcb);
    void SetNOWsntCB(TxCb sntcb);
    void Begin();
    void Loop();
};

void MyClass::SetNOWrcvCB(RxCb rcvcb) {
  NOWrcvCB = rcvcb;
}
void MyClass::SetNOWsntCB(TxCb sntcb) {
  NOWsntCB = sntcb;
}
void MyClass::Begin() {
  // Code will go here to actually check if ESP-NOW is enabled in the config and start it up
}
void MyClass::Loop() {
  // Code to potentially run at loop time
}



MyClass abc;

void OnDataRecv(const esp_now_recv_info_t *esp_now_info, const uint8_t *data, int data_len) {
  // User-provided callback
}

void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  // User-provided callback
}

void setup() {
  abc.SetNOWrcvCB(OnDataRecv);  // Override default callback
  abc.SetNOWsntCB(OnDataSent);  // Override default callback
  abc.Begin();
}
void loop() {
  abc.Loop();
}

Ahh, I forgot about that and didn't realize it would affect this. As the function does not access anything else within the class and just prints the data received to serial (essentially for debugging), I supposed I could make it a static void as well.

That would also work.

How does the User select what function to run?

This seems like a job for a state machine.

let member functions start with small letters setxxx, begin(), loop()...

I am unsure of what you mean by this.

The user would not have to "select" a function. There will be a default function within my code that will simply print the ESP-NOW data to the serial port. Sort of a built in debugging mode to see what is being received without writing any functions yet.

All the user would have to do to override the default function would be to run the function to set the callback to their own function.

Other than people thinking that this should be the proper way to do things, can you tell me if there is a legitimate reason for doing this? I am not trying to be mean, I am genuinely curious because it doesn't seem to make a difference to the compiler. So, if it doesn't matter to the compiler, why does it actually matter?

that is the reason!
Most people stick to this convention and are used to it. You want to write a library so I recommend to use what people know.

consistency: if you apply common rules you are less likely to fall into typos / erros on your own. See your own not working example:

it would be correct if you would apply the common rule.

You should consider to read about how to write libraries:
from Sparkfun:

and the Arduino APIStyleGuide itself:
https://docs.arduino.cc/learn/contributions/arduino-library-style-guide/

Okay, so no real valid reason there. As far as I'm concerned, whoever uses the code can either read whatever directions I write, or follow the code themselves to make sure they have upper and lower case correct. This is trivial and not really anything to do with what I was really asking about. This would not make the code any better, just make it so that people with OCD feel better about it.

This was literally something I typed out as an example and not necessarily actual code, which should be obvious by the fact that the class is named MyClass and not what I really named it. I did indeed also say that the code I had compiled, which is another obvious clue that this wasn't the exact code I was using. So, it should be obvious that it was just a typo on here and thus your statement is not valid.

Here's an interesting fact. One of these guides refers to using camel case for naming things. However, in camel case, the first word can begin with upper or lower case while every additional word starts with an uppercase letter to show that it is a new word. If you want to be more specific, or rather if the documents wanted to be more specific, they could have specified starting with lower case, but they didn't. The history of camel case is interesting.

The only mention of "camel case" in the Arduino Style Guide for Creating Libraries I found is:

They gave examples for functions in camel case starting with a small letter.

But anyhow.

If you want to break with common practice and make your code more obscure for others to use - go ahead. It's your code.

How is that achieved without the user having a choice to override?

If the User does nothing, the NOW is invoked.
Else --- well the User has choice or not, somehow without actually having one?
That plan has a hole in it.

Capitalize Constants. camelCase variable. CamelCase Constant

look this over

output

myRecv
mySend

more conventionally named symbols

#include <stdio.h>
#include <stdint.h>

// -----------------------------------------------------------------------------
typedef int esp_now_recv_info_t;
typedef int esp_now_send_status_t;

class MyClass {
  public:
	void (*nowRcvCb) (const esp_now_recv_info_t*, const uint8_t*, int);
	void (*nowSntCb) (const uint8_t*,             esp_now_send_status_t);

    MyClass () { }

	void setNowRcvCb (
        void (*f) (const esp_now_recv_info_t*, const uint8_t*, int))
    {
	    this->nowRcvCb = f;
    }
	void setNowSntCb (void (*f) (const uint8_t*, esp_now_send_status_t))
    {
	    this->nowSntCb = f;
    }

	void begin () { };
	void loop  () { };
};

MyClass one;

// -----------------------------------------------------------------------------
void myRecv (
    const esp_now_recv_info_t *info,
    const uint8_t             *buf,
    int                        nByte)
{
    Serial.println (__func__);
}

// ---------------------------------------------------------
void mySend (
    const uint8_t*         buf,
    esp_now_send_status_t  status )
{
    Serial.println (__func__);
}

// -----------------------------------------------------------------------------

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

    one.setNowRcvCb (myRecv);
    one.setNowSntCb (mySend);

    one.nowRcvCb (0, 0, 0);
    one.nowSntCb (0, 0);
}

void loop () {}

Are you reading what you wrote? the key word in that statement is "examples". They gave an example of using camelcase instead of an underscore, they did not specify if it is standard to use upper or lower case in the first word.

How in the world does this make anything more obscure? I believe that you may need to look up the definition of obscure. There is nothing unclear, concealed or misunderstood by using an uppercase letter instead of lowercase on the first word.

I mean, honestly, if you're using someone else's code, you're either going to read their code and see how they wrote it, or you're going to read the documents on how to use it, in which hopefully they wrote them well, in which case everything would be right in front of you and not unclear or concealed in any way.

As I previously noted, and I wrote some initial hypothetical code for that was incomplete, there will be a function pointer that is either set by default to an internal function or set to nullptr. if nullptr, then upon starting the ESPNOW, it would check if it's still null (not set by user before starting) and assign the pointer to the internal function. If preset to a default, then the user could simply call a function within my class to reassign the function pointer to their own function. I'm still working on things a bit and I've been really busy lately, but I will try to post some actual code (as opposed to hypothetical examples) when I get a chance.