Help with function pointers (I think!)

I am not sure if I need to understand function pointers a bit better here but I'll try and explain the problem I am having.

I have a project where I need upwards of 64 input buttons. I have used the excellent OneButton FSM library (GitHub - mathertel/OneButton: An Arduino library for using a single button for multiple purpose input.) in the past for de-bouncing, called via a timer ISR and it works fine. Now that I have so many input buttons it all becomes a little more complex. Instead of just one or two callback functions that I have previously just duplicated code in, now I am looking at having near 64 identical callback functions. In my final project a button press should generate a CANbus message. In my old way of doing things I would have to have 64 functions that only differed by a CANbus ID.

What I think I need to do is pass a function pointer into the button object instantiation that includes a button ID argument but I don't know how to do this or if I need to modify the OneButton class to achieve this.

The OneButton class includes this:

// ----- Callback function types -----

extern "C" {
typedef void (*callbackFunction)(void);
}

I don't really understand this bit of code but I assume the callback function that the class expects does not allow me to pass in an argument for my button ID.

My test code:

#include "OneButton.h"

typedef enum {
  ACTION1_OFF,
  ACTION1_ON,
  ACTION2_OFF,
  ACTION2_ON
} 
buttonActions;
  
OneButton button1(10, true);
OneButton button2(8, true);

buttonActions nextAction1 = ACTION1_OFF;
buttonActions nextAction2 = ACTION2_OFF;

#define LED1 (9)
#define LED2 (7)

void setup() {
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
   
  button1.attachClick(myClickFunction1);
  button1.setClickTicks(200);
  button1.setPressTicks(5000);
  
  button2.attachClick(myClickFunction2);
  button2.setClickTicks(200);
  button2.setPressTicks(5000);
}


void loop() {
  unsigned long now = millis();

  button1.tick();
  button2.tick();

  if (nextAction1 == ACTION1_OFF) {
    digitalWrite(LED1, LOW);
  } else if (nextAction1 == ACTION1_ON) {
    digitalWrite(LED1, HIGH);
  } 
  
  if (nextAction2 == ACTION2_OFF) {
    digitalWrite(LED2, LOW);
  } else if (nextAction2 == ACTION2_ON) {
    digitalWrite(LED2, HIGH);
  }
}


void myClickFunction1() {
  if (nextAction1 == ACTION1_OFF) {
    nextAction1 = ACTION1_ON;
  } else {
    nextAction1 = ACTION1_OFF;
  }

  //send CANbus message for the specific button ID and include state information
}


void myClickFunction2() {
  if (nextAction2 == ACTION2_OFF) {
    nextAction2 = ACTION2_ON;
  } else {
    nextAction2 = ACTION2_OFF;
  }

  //send CANbus message for the specific button ID and include state information
}

Can anyone give me some pointers (no pun intended) on how I can have the callback function allow me to pass in arguments and prevent me having to write 64 separate callback functions.

In my old way of doing things I would have to have 64 functions that only differed by a CANbus ID.

Why not just one function that takes the CANbus ID as a parameter ?
Read the buttons and determine which became pressed. Use that as the index to an array of corresponding CANbus IDs

Which Arduino board are you using, how are the 64 buttons connected to it and how do you propose to determine which has become pressed ? The answer is surely not 64 if statements, or is it ?

Why not just one function that takes the CANbus ID as a parameter ?
Read the buttons and determine which became pressed.

Because the library uses callback functions so that I don't have to read 64 buttons within the loop to determine if 1 has changed.

Which Arduino board are you using, how are the 64 buttons connected to it and how do you propose to determine which has become pressed ? The answer is surely not 64 if statements, or is it ?

Not that it matters for the purpose of the question but I am using 2 x NXP PCA9698 40-bit I/O expanders to handle the I/O. I am prototyping on an Arduino Nano but I will be implementing on a Teensy 4.0. The whole point of using call;back functions is so that I only execute something when a button event happens. I thought that the best way of handling this is to pass in a callback function at instantiation time that has a parameter for the ID of the button being pressed or maybe better, pass in a pointer to an object method for a particular button.

The library on GitHub supports parameterized callback functions. When called, your callback will be passed a void *. This is customizable on a per-button basis and you can have it point to anything you want. Could be an array element identifying the button or even the button instance itself.

gfvalvo:
The library on GitHub supports parameterized callback functions. When called, your callback will be passed a void *. This is customizable on a per-button basis and you can have it point to anything you want. Could be an array element identifying the button or even the button instance itself.

But doesn't the library only accept a pointer with a (void) parameter?

typedef void (*callbackFunction)(void);

I would need to pass in a function pointer with a parameter to refer somehow to the actual button when its instantiated wouldn't I? Thats the basis of my original question.

solderdog:
But doesn't the library only accept a pointer with a (void) parameter?

void *p means p is a pointer to an arbitrary object - p could point to a string, array, struct, float, etc. This means you can pass a single object of any type to your callback function.

solderdog:
I would need to pass in a function pointer with a parameter to refer somehow to the actual button when its instantiated wouldn't I? Thats the basis of my original question.

You could have a single callback function that accepts the pin-number as a parameter. Then you would automatically know which button was pressed without testing them all. Something like this:

button1.attachClick(myClickFunction, 10);

Where:

void myClickFunction(const uint8_t &pinNum) {
  //find button state?
  //send CANbus message for the specific button ID and include state information
}

Power_Broker:
void *p means p is a pointer to an arbitrary object - p could point to a string, array, struct, float, etc. This means you can pass a single object of any type to your callback function.

There are 2 'voids' in that class. Doesn't the first one mean there is no return? What does the second one mean?

typedef void (*callbackFunction)(void);

Power_Broker:
You could have a single callback function that accepts the pin-number as a parameter. Then you would automatically know which button was pressed without testing them all. Something like this:

button1.attachClick(myClickFunction, 10);

The library would still need to be modified:

/Users/xxx/Documents/Arduino/libraries/OneButton/src/OneButton.h:56:8: note:   candidate expects 1 argument, 2 provided
exit status 1
no matching function for call to 'OneButton::attachClick(void (&)(), int)'

I have written a test class to try and understand how to pass in the function. I'll post below.

So I have written a test class to try and understand how to pass in the function and an id for each button. I think I will have to write a wrapper class around the OneButton library to make it do what I think I need it to do!

Class: test1

test1.h

#include "Arduino.h"

extern "C" {
typedef void (*callbackFunction)(int);
}

class test1
{
public:
    //constructor
    test1(int pin, callbackFunction callback);

    void begin();

private:
  int _pin; // button pin
  callbackFunction _callback = NULL; // callback function pointer
};

test1.cpp

#include "test1.h"

test1::test1(int pin, callbackFunction callback) {
    _pin = pin;
    _callback = callback;
}

void test1::begin() {
    delay(2000);
    _callback(_pin);
}

My test sketch:

#include "test1.h"

void myFunc(int pinID) {
  Serial.print("Button ID: ");
  Serial.println(pinID);   
}

void setup() {
  Serial.begin(115200);
  Serial.println("Start");

  test1 button1(99, myFunc);
  test1 button2(33, myFunc);

  button1.begin();
  button2.begin();
}

void loop() {
  
}

This produces the expected results:

Start
Button ID: 99
Button ID: 33

No need for a wrapper library! Just try something like this:

#include "OneButton.h"


const uint8_t BUTTON_PIN = 19;
OneButton button(BUTTON_PIN, true);


void setup()
{
  Serial.begin(115200);
  button.attachClick(clickFunc, BUTTON_PIN);
}
  

void loop()
{
  button.tick();
  delay(10);
}


void clickFunc(uint8_t pinNum) // <-- slightly edited from my original proposal
{
  Serial.print("pressed: "); Serial.println(pinNum);
}

It does not work unfortunately. As I've been saying the library typedef expects 1 parameter which is a pointer.

candidate expects 1 argument, 2 provided
/Users/xxxx/Documents/Arduino/sketch_apr16b/sketch_apr16b.ino: In function 'void setup()':
sketch_apr16b:11:43: error: no matching function for call to 'OneButton::attachClick(void (&)(uint8_t), const uint8_t&)'
   button.attachClick(clickFunc, BUTTON_PIN);
                                           ^
In file included from /Users/xxxx/Documents/Arduino/sketch_apr16b/sketch_apr16b.ino:1:0:
/Users/xxxx/Documents/Arduino/libraries/OneButton/src/OneButton.h:56:8: note: candidate: void OneButton::attachClick(callbackFunction)
   void attachClick(callbackFunction newFunction);
        ^
/Users/xxxx/Documents/Arduino/libraries/OneButton/src/OneButton.h:56:8: note:   candidate expects 1 argument, 2 provided
exit status 1
no matching function for call to 'OneButton::attachClick(void (&)(uint8_t), const uint8_t&)'

Obviously, you need to rethink your approach. This compiles but is untested:

#include "OneButton.h"

void callBack(void *p);

class NewButton : public OneButton {
  public:
    NewButton(uint8_t pin) : OneButton(pin, true) {
    }

    void attach(parameterizedCallbackFunction f) {
      OneButton::attachClick(f, this);
    }

    void doSomething() {
    }

};

NewButton b1(8);
NewButton b2(10);

void setup() {
  b1.attach(callBack);
  b2.attach(callBack);
}

void loop() {
}

void callBack(void *p) {
  ((NewButton *) p)->doSomething();
}

Just as obviously, if you're going to have 64 buttons, then don't instantiate button1, button2, button3, ......
Use an array.

Try something like this:

void clickFunc( void * pinNum )
{
  Serial.print("pressed: "); Serial.println( *(uint8_t*)pinNum );
}

I have created a wrapper class - OneButtonTwo

OneButtonTwo.h

#include "Arduino.h"
#include "OneButton.h"

extern "C" {
typedef void (*cbFunct)(int, int);
}

class OneButtonTwo
{
public:
    //constructor
    OneButtonTwo(int pin, int id, cbFunct callback);
    void begin();
    void tick();

private:
    int _pin; // button pin
    int _id;
    cbFunct _callback = NULL; // callback function pointer
    OneButton button;

    void createMsg();
};

OneButtonTwo.cpp

#include "OneButtonTwo.h"

OneButtonTwo::OneButtonTwo(int pin, int id, cbFunct callback) {
    _pin = pin;
    _id = id;
    _callback = callback;
    OneButton button(_pin, true);
}

void OneButtonTwo::createMsg() {
    _callback(_pin, _id);
}

void OneButtonTwo::begin() {
    //button.attachClick(OneButtonTwo::createMsg);
    button.setClickTicks(200);
    button.setPressTicks(5000);
    _callback(_pin, _id);
}

void OneButtonTwo::tick() {
    button.tick();
}

My test sketch:

#include "OneButtonTwo.h"

#define B1_PIN  5
#define B2_PIN  4

void myFunc(int pinButton, int pinID) {
  Serial.print("Button ID: ");
  Serial.println(pinID);   
}

OneButtonTwo button1(B1_PIN, 0, myFunc);
OneButtonTwo button2(B2_PIN, 1, myFunc);

void setup() {
  Serial.begin(115200);
  Serial.println("Start");

  button1.begin();
  button2.begin();
}

void loop() {
  button1.tick();
  button2.tick();
}

This works, but it errors when I uncomment the callback handler in OneButtonTwo.cpp

/Users/xxxx/Documents/Arduino/libraries/OneButtonTwo/OneButtonTwo.cpp: In member function 'void OneButtonTwo::begin()':
/Users/xxxx/Documents/Arduino/libraries/OneButtonTwo/OneButtonTwo.cpp:15:47: error: invalid use of non-static member function
     button.attachClick(OneButtonTwo::createMsg);
                                               ^
exit status 1
Error compiling for board ATmega328.

I don't yet understand what "invalid use of non-static member function" means.

solderdog:
It does not work unfortunately. As I've been saying the library typedef expects 1 parameter which is a pointer.

Idk what to tell you - the code I sent in my last post compiled and operated correctly for my Mega...

Not sure why you're hung up on the typedef when what you should be looking at is this:

void attachClick(parameterizedCallbackFunction newFunction, void* parameter);

solderdog:
So I have written a test class to try and understand how to pass in the function and an id for each button. I think I will have to write a wrapper class around the OneButton library to make it do what I think I need it to do!

i don't see any other option if you want to use that library. you may be better off writing your own.

if the callback has an integer argument, the presumption is that the library would call the function with the button id as the argument. But the library is designed to call a unique funciton.

Power_Broker:
Idk what to tell you - the code I sent in my last post compiled and operated correctly for my Mega...

Not sure why you're hung up on the typedef when what you should be looking at is this:

void attachClick(parameterizedCallbackFunction newFunction, void* parameter);

I am not sure what you are saying.

Where did this line come from?

void attachClick(parameterizedCallbackFunction newFunction, void* parameter);

Are you telling me to modify the OneButton library? I know I could have done this, but I was trying not to modify the base library and thought I would have to write a wrapper to make it do what I wanted it to do.

Why have you rejected the suggestion in Reply 11?

gfvalvo:
Why have you rejected the suggestion in Reply 11?

I haven't got that far yet, I can only reply once every 5mins because of the damn restrictions in this forum.