How to cleanly deal with interrupts and member functions?

Scope: I know this rough question has been around a few times. Once again, I would like to attach a non-static member function to an interrupt, but I know that is not directly possible.

Details: I am trying to write a lib for reading PWM signals. In particular, I want to read three PWM channels from an RGB controller. I presume that they are in sync, but lacking a multi channel oscilloscope, I do not know that for certain, so the plan is to read the three inputs in series, to get rid of problems with lost interrupt calls at both simultaneously rising and falling flanks.

Since I have multiple objects, the usually suggested hacks with global functions are not really feasible, at least not without severely cluttering my main code, which is already long enough. (I actually started the lib because I lose overview, due to a lack of keyword highlighting, a function index and all that stuff in the Arduino IDE.)

Any ideas how to solve this with minimal code in the main sketch? I am far from a C++ pro, actually I looked up syntax in a few other libs, but I do know OOP from other ... more forgiving languages.

Here is the code of how it would look if I COULD supply member functions to attachInterrupt. Note that I am on an ESP, so my interrupts are addressed with pin numbers and all pins support interrupts.

header:

#ifndef PWMREADER_H
#define PWMREADER_H

#if defined(ARDUINO) && ARDUINO >= 100
#include "Arduino.h"
#else
#include "WProgram.h"
#endif

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

class PWMReader
{
	public:
		PWMReader(const unsigned int pwmPinAttr);      // Constructor
                
                void attachCallback(callbackFunction newFunction);
                void probe();
                void rising1InterrFunc();
                void rising2InterrFunc();
                void fallingInterrFunc();
		
		uint8_t pwmPin;
                volatile unsigned long pwm_value = 0;
                volatile unsigned long riseTime = 0;
                volatile unsigned long fallTime = 0;
	private:
		callbackFunction _valueFunc;
};
#endif

cpp:

#if defined(ARDUINO) && ARDUINO >= 100
#include "Arduino.h"
#else
#include "WProgram.h"
#endif

#include "PWMReader.h"

PWMReader::PWMReader(const unsigned int pwmPinAttr)
{
    pwmPin = pwmPinAttr;
        
}

void PWMReader::attachCallback(callbackFunction newFunction)
{
    _valueFunc = newFunction;
}

void PWMReader::probe()
{
    attachInterrupt(pwmPin,PWMReader::rising1InterrFunc,RISING);
}

void PWMReader::rising1InterrFunc()
{
    riseTime = micros();
    attachInterrupt(pwmPin,fallingInterrFunc,FALLING);
}
void PWMReader::fallingInterrFunc()
{
    fallTime = micros();
    attachInterrupt(pwmPin,rising2InterrFunc,RISING);
}
void PWMReader::rising2InterrFunc()
{
    unsigned long now = micros();
    
    pwm_value = (fallTime - riseTime) * 100 / (now - riseTime);
    if (_valueFunc) _valueFunc();
    
    detachInterrupt(pwmPin);
}

Example sketch:

#include <PWMReader.h>

PWMReader pwmreader[] = {{D1}, {D2}, {D3}};
const uint8_t pwmreader_N = 3;
uint8_t pwmreader_current = 0;


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

  for (int i = 0; i < pwmreader_N; i++)
    pwmreader[i].attachCallback(processNewPWM);

  pwmreader[0].probe();
}

void loop() {

}

void processNewPWM() {
  if (pwmreader_current == pwmreader_N-1)
  {
    for (int i = 0; i < pwmreader_N; i++)
    {
      Serial.print(pwmreader[i].pwm_value); // Output for Serial plotter
      Serial.print(" ");
    }
    Serial.println();
  }
  pwmreader_current = (pwmreader_current+1)%pwmreader_N;
  pwmreader[pwmreader_current].probe();
}

This is not primarily a learning project for me (though I take what I get!), so a ready made lib which deals with multiple, potentially simultaneous PWM signals would also be highly appreciated.

You should not be doing Serial.print() in an interrupt handler.

What is generating the interrupt?

An interrupt is like a doorbell ringing.

Suppose that you have 3 kids at home. The doorbell rings. Which one of them should go answer the door? Do you expect all three of them to do that?

PaulS:
You should not be doing Serial.print() in an interrupt handler.

Ok, good remark, I overlooked that while quickly putting together an example. Of course, this does not happen in my productive code. Here, it should not be a problem, since at that time, not interrupts are attached and the program does not have any other routine.

PaulS:
What is generating the interrupt?

As I described in the initial post, a PWM controller for an RGB led. Just assume there are clean (and potentially correlated, to flanks might happen exactly at the same time with quite some probability) PWM signals on pins D1,D2,D3.

PaulS:
An interrupt is like a doorbell ringing.

Suppose that you have 3 kids at home. The doorbell rings. Which one of them should go answer the door? Do you expect all three of them to do that?

I do not understand that remark. If you didn't see a bug that I did not see, there should only be one interrupt attached at a given time, to a specified pin. That is why I wrote that I probe them in series.
The process starts at D1, waits for a rising flank, waits for a falling flank (determines the puls length), waits for a rising flank again (determines the overall frequency), then detaches the interrupt and calls the callback. The callback then starts the same process with the next pin and so on.

So, what IS the problem you are trying to solve?

If there is only one instance of the class that cares about the interrupt at a time, then the analogy I was making IS appropriate.

The interrupt happens (the doorbell rings). At any given time, only one of your kids is the designated door opener, so when the interrupt happens, that instance of the class Kid (that particular kid) answers the door.

Of course, somehow someone needs to keep track of which instance of the Kid class (which particular kid) is on duty at a given time.

Typically, the class would have a static method that responded to the interrupt. The class would also have a static method that lets any given instance register as the interested instance.

The static answerTheDoor() method would then call the registered instance's method to do the real work.

Suppose that you have three kids, Mary, Joe, and Ahmad.

Suppose that the Kid class has a static answerTheDoor() method, and a static onAnswerTheDoorDuty() method that takes a Kid *, and a private field, of type Kid *, called whoIsOnDuty.

Suppose that it is Ahmad's turn to answer the door. Ahmad would call the onAnswerTheDoorDuty() method, passing it this (a pointer to himself).

Suppose the UPS guy rings the doorbell. The Kid::answerTheDoor() method hears the doorbell ring, and calls whoIsOnDuty->answerTheDoor().

Since whoIsOnDuty is a pointer that currently points to Ahmad, Ahmad's answerTheDoor() method is called, and Ahmad answers the door.

At some other time, Sue calls the onAnswerTheDoorDuty() method, passing it this (a pointer to herself).

The FedEx guy rings the bell. The Kid::answerTheDoor() method hears the doorbell ring, and calls whoIsOnDuty->answerTheDoor().

Since whoIsOnDuty is a pointer that now points to Sue, Sue's answerTheDoor() method is called, and Sue answers the door.

Ok, now I see where you were getting at. Hm, that would essentially mean to move the storage of the instances and the tracking of the current instance into the static part of the class, such that it can be forwarded to the appropriate instance. That would be a possibility. Thanks. I would have preferred a class that that encapsulates a single process, though. With this solution, the commissioning class would deal with the interrupt switchover.