Tutorial how to setup an event based sketch

Hi, just wanted to demonstrate that it's possible to write code for arduino more naturally using classes and events. The working sketch is here PinEvent - Wokwi ESP32, STM32, Arduino Simulator

Below is the code. It creates a class to represent a DigitalPin. This class has an event OnChange. A Button class is simply a DigitalPin in INPUT_PULLUP mode. An Led is a digital pin in OUTPUT mode. Led "listens" to the Button event and lights up if the button is pressed.

#include "Eventfun.h"

////////////////////////////////////
// definition for DigitalPin class
///////////////////////////////////

// define a class representing digital pin
template<typename T>
class DigitalPin {
protected:
  int _pin; // pin number
  bool _value; // last value read from the pin
  unsigned long _msDebounce;
  unsigned long _ms;
  bool _debouncing;
public:
  // declare event
  typedef EventDelegate<T, bool> ChangeEvent;
  ChangeEvent OnChange;

public:
  // initialize DigitalPin object
  DigitalPin(int pin, int pin_mode, int msDebounce = 0) 
    : _pin(pin), _msDebounce(msDebounce), _debouncing(false) {
    pinMode(_pin, pin_mode);
    if (pin_mode != OUTPUT) {
      _value = digitalRead(_pin);
    }
  }

  // read the pin value and only trigger the event if there is a change
  void Update() {    
    auto value = digitalRead(_pin);    
    if (value != _value) {
      if (_debouncing && millis() - _ms > _msDebounce)
      {
        _debouncing = false;
        _value = value; // update to the new value
        OnChange((T*)this, _value); // trigger event
      }
      else if (!_debouncing) {
        _ms = millis();
        _debouncing = true;
      }
    }      
  }

  // accessor for _pin
  int Pin() {
    return _pin;
  }
  
  // for convenience overload () operator to set value
  operator()(bool value) {
    _value = value;
    digitalWrite(_pin, value);
  }

  // for convenience overload bool operator to return value
  operator bool() {
    return _value;
  }
};


// now that we have a class representing a digital pin
// we can create classes that use digital pin

// a button
class Button: public DigitalPin<Button> {
public:
  Button(int pin) : DigitalPin(pin, INPUT_PULLUP, 100) {
  }
};

// a LED
class Led: public DigitalPin<Led> {
public:
  Led(int pin, Button& button) : DigitalPin(pin, OUTPUT) {
    button.OnChange = Button::ChangeEvent(this, &Led::onButtonClick);
  }

  void onButtonClick(Button* sender, bool value) {
    Serial.println("hello");
    operator()(!value);
  }
};

// it may look like too much for a simple project but when you have many 
// interconnected components that need to react to changes and  interact with one another
// then using classes with events may simplify your program:

//////////////////
// main program
/////////////////

Button redButton(12);
Button greenButton(2);

Led redLed(redButton.Pin() + 1, redButton);
Led greenLed(greenButton.Pin() + 1, greenButton);

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

void loop() {
  redButton.Update();
  greenButton.Update();
}

For the sake of completeness, it’s not bad to add the file

Eventfun.h

Eventfun is a library that provides easy to use event delegates. But the lbrary's code itself is not really beginner friendly...

#ifndef __EVENTFUN_H__
#define __EVENTFUN_H__

#include "BPtr.h"
#include "BList.h"

template<typename TSender, typename TArgument>
class EventDelegate {
protected:
  struct Callable {
    virtual ~Callable() {}
    virtual void Call(TSender* sender, TArgument argument) = 0;
    virtual char Type() const = 0;
    virtual bool operator == (const Callable& other) const = 0;
  };

  template<typename TClass>
  struct CallableMethodImpl : public Callable {
    TClass* _instance;
    void (TClass::*_method)(TSender*, TArgument);

    CallableMethodImpl(TClass* instance, void (TClass::*method)(TSender*, TArgument))
      : _instance(instance), _method(method) {}

    virtual void Call(TSender* sender, TArgument argument) {
      (_instance->*_method)(sender, argument);
    }

    virtual char Type() const {
      return 'M';
    }

    virtual bool operator == (const Callable& other) const {
      if (Type() != other.Type()) return false;
      auto callable = (const CallableMethodImpl<TClass>&)other;
      return _instance == callable._instance && _method == callable._method;
    }
  };

  struct CallableFunctionImpl : public Callable {
    void (*_func)(TSender*, TArgument);

    CallableFunctionImpl(void (*func)(TSender*, TArgument))
      : _func(func) {}

    virtual void Call(TSender* sender, TArgument argument) {
      (_func)(sender, argument);
    }

    virtual char Type() const {
      return 'F';
    }

    virtual bool operator == (const Callable& other) const {
      if (Type() != other.Type()) return false;
      auto callable = (CallableFunctionImpl&)other;
      return _func == callable._func;
    }
  };

protected:
  Buratino::BPtr<Callable> _callable;  // smart pointer

public:
  typedef TSender SenderType;
  typedef TArgument ArgumentType;

  template<typename TClass>
  EventDelegate(TClass* instance, void (TClass::*method)(TSender* sender, TArgument argument))
    : _callable(new CallableMethodImpl<TClass>(instance, method)) {}

  EventDelegate(void (*func)(TSender* sender, TArgument argument))
    : _callable(new CallableFunctionImpl(func)) {}

  EventDelegate()
    : _callable(0) {}

  ~EventDelegate() {
  }

  void operator()(TSender* sender, TArgument argument) {
    if (_callable) {
      _callable->Call(sender, argument);
    }
  }

protected:
  explicit operator bool() const {
    return (bool)_callable;
  }

  bool operator == (const EventDelegate<TSender, TArgument>& other) const {
    return *_callable == *other._callable;
  }

  template<typename, typename>
  friend class EventSource;
};

template<typename TSender, typename TArgument>
class EventSource
{
protected:
  Buratino::BList<EventDelegate<TSender, TArgument>> _list;

public:
  EventSource(int numEvents = 3) 
    : _list(numEvents) {}
  
  void operator += (EventDelegate<TSender, TArgument> delegate)
  {
    auto k = _list.Length();
    for(unsigned i = 0; i < _list.Length(); ++i) {
      if (_list[i] == delegate) {
        return;
      }
      if (!_list[i]) {
        k = i;
      }
    }

    if (k == _list.Length()) {
      _list.Add(delegate);
    }
    else {
      _list[k] = delegate;
    }
  }

  void operator += (void (*delegate)(TSender*, TArgument)) {
    this += EventDelegate<TSender, TArgument>(delegate);
  }

  void operator -= (EventDelegate<TSender, TArgument> delegate) {
    for (unsigned i = 0; i < _list.Length(); ++i) {
      if (_list[i] == delegate) {
        _list[i] = EventDelegate<TSender, TArgument>();
      }
    }
  }

  void operator -= (void (*delegate)(TSender*, TArgument)) {
    this -= EventDelegate<TSender, TArgument>(delegate);
  }

protected:
  void operator () (TSender* sender, TArgument argument) {
    for(unsigned i = 0; i < _list.Length(); ++i) {
      _list[i](sender, argument);
    }
  }

  friend TSender;
};

#endif

This is a sketch for a more practical application to control an LED via a push button without using a special library to follow the KISS principle.

#define ProjectName "BUTTON2PIN"
//variables 
constexpr uint8_t Inputs[] {A0};
constexpr uint8_t Outputs[] {9};
//structures
struct BUTTON2PIN 
{
  uint8_t button;
  uint8_t led;
  void make(uint8_t button_, uint8_t led_)
  {
    button=button_;
    pinMode(button,INPUT_PULLUP);
    led=led_;
    pinMode(led,OUTPUT); 
  }
  void update()
  {
    digitalWrite(led,digitalRead(button)?LOW:HIGH);
  }
}button2pins[sizeof(Inputs)]; 

//support 
void heartBeat(int LedPin, uint32_t currentMillis)
{
  static bool setUp = false;
  if (!setUp) pinMode (LedPin, OUTPUT), setUp = !setUp;
  digitalWrite(LedPin, (currentMillis / 500) % 2);
}

void setup()
{
  Serial.begin(115200);
  Serial.println(ProjectName);
  uint8_t element=0;
  for (auto &button2pin:button2pins)
  {
    button2pin.make(Inputs[element],Outputs[element]);
    element++;
  }
}

void loop() 
{
  uint32_t currentMillis=millis(); 
  heartBeat(LED_BUILTIN, currentMillis);
  for (auto &button2pin:button2pins) button2pin.update();
}

Have a nice day and enjoy coding in C++.

2 Likes

You are not using C++ at all. You use structural programming instead of OOP and you don’t use eventing paradigm. So your example is just and old way of doing things… Of course it still works

1 Like

up to you

Have a nice day and enjoy coding in C++.

A useless utterance

1 Like

Neither is the example. Good luck getting a beginner to write a sketch like that.
nevermind helping them fix it when it doesn't work!

1 Like

Yes it is pretty comprehensive, with debouncing too. But I think the starting point is creating a class that represents an IO pin, then Update it in the main loop and let it trigger events to which other objects subscribe.. I wanted to show that this could be a design for an event driven sketch..

While I applaud the effort and skills.Speaking as someone who believes I am at the tipping point. I think this library is too advanced for beginners and not needed by those advanced enough to utilize it.

2 Likes

Hello @Hutkikz and others - I struggled (and still struggle) with event timers. Is this (below) event timer too complicated for beginners? It shows two events of different periods that meet over a few cycles, then repeat. The simulation is here.

// https://wokwi.com/projects/366159375120487425

// each character on this timing graph represents 50ms.
// interval[0] flops between 0 and 1, interval[1] flops between 2 and 3, simultaneous gets a 4
// interval[0] = 200ms  |---*---*---*---*---*---*---*---*---*---*---*---*---*---*---*...
// interval[1] = 500ms  |---------*---------*---------*---------*---------*---------*...
// timer = i0*i1=1000ms |---0---1-2-0---1---4---1---0-2-1---0---4---0---1-2-0---1---4...

unsigned long interval[] = {200, 500}; // set two event intervals
unsigned long previousTime[] = {0, 0}; // clear two event timers
unsigned long currentTime; // use for all intervals

bool intervalEvent[2]; // two events
bool intervalOccurred[2]; // two events happened
int intervalCount[2] = {0, 2}; // two event starting values

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

void loop() {
  unsigned long currentTime = millis(); // update "now" time for all intervals and timers

  if (currentTime - previousTime[0] >= interval[0]) { // 200ms interval
    previousTime[0] = currentTime; // event occurred, set new start time
    intervalOccurred[0] = 1; // flag for simultaneous events
  }

  if (currentTime - previousTime[1] >= interval[1]) { // 500ms interval
    previousTime[1] = currentTime; // event occurred, set new start time
    intervalOccurred[1] = 1; // flag for simultaneous events
  }

  if (intervalOccurred[0] && intervalOccurred[1]) { // 200ms and 500ms intervals together
    intervalOccurred[0] = 0; // clear flag for 200ms event
    if (intervalCount[0] == 0) { // if 200ms count is 0
      intervalCount[0]++; // increase count - if it is 0 change it to 1
    }
    else if (intervalCount[0] == 1) { // if 200 count is 1
      intervalCount[0]--; // decrease count - if it is 1 change it to 0
    }

    intervalOccurred[1] = 0; // clear flag for 500ms event
    if (intervalCount[1] == 2) { // if 500 count is 2
      intervalCount[1]++; // increase count
    }
    else if (intervalCount[1] == 3) { // if 500 count is 3
      intervalCount[1]--; // decrease count
    }
    Serial.print(4);
  }

  if (intervalOccurred[0]) { // 200ms event has occurred
    intervalOccurred[0] = 0; // clear flag
    if (intervalCount[0] == 0) { // if count is 0
      Serial.print(0); // print count
      intervalCount[0]++; // increase count
    }
    else if (intervalCount[0] == 1) { // if count is 1
      Serial.print(1); // print count
      intervalCount[0]--; // decrease count
    }
  }

  if (intervalOccurred[1]) { // 500ms event has occurred
    intervalOccurred[1] = 0; // clear flag
    if (intervalCount[1] == 2) { // if count is 2
      Serial.print(2); // print count
      intervalCount[1]++; // increase count
    }
    else if (intervalCount[1] == 3) { // if count is 3
      Serial.print(3); // print count
      intervalCount[1]--; // decrease count
    }
  }
}

void welcome() {
  Serial.println("Each character on this timing graph represents 50ms.");
  Serial.println("interval[0] flops 0 and 1 every 200ms   |---*---*---*---*---*---*---*---*---*---*---*---*---*---*---*...");
  Serial.println("interval[1] flops 2 and 3 every 500ms   |---------*---------*---------*---------*---------*---------*...");
  Serial.println("interval[0] & interval[1] = 4 at 1000ms |---0---1-2-0---1---4---1---0-2-1---0---4---0---1-2-0---1---4...");
}
1 Like

Looks like some specific algorithm to solve a task. Maybe even a design pattern for similar algorithms. But it’s something different from event delegates. You can trigger an event via a delegate in your code too though with Eventfun

Yes, it was, and I agree the extra "complexity" detracts from the purpose. What would be a good replacement for the event(s)?

Event delegates? Can you explain this? Thank you.

A delegate is just a fancy function pointer. While a function pointer is a direct memory address a delegate is a wrapper class around the function call. In this case the reason for the wrapper class is mainly to allow not just functions but class methods to be called as callbacks which is not straightforward in C++. And it’s specifically called event delegates just because it’s used for the purpose of setting up callback connections between objects and typically these callbacks are called events because of how they are used. So I think the word event is used in different contexts here..

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.