Table of Contents

Jalon 4: Ajouter à la classe Button la réactivité

Retour à la séance 6

Dans l'exemple précédent, nous appelons une fonction externe à la classe Button. En fait, nous aimerions avoir une classe Button à laquelle nous pourrions ajouter le comportement que doit avoir l'objet quand un événement se produit, comme par exemple appuyer sur le bouton ou relâcher le bouton.

Question n°1

Ajouter trois méthodes virtuelles à votre classe Button qui correspondent à :

ces méthodes sont vides.

Question n°2

La difficulté vient du fait que la méthode attachInterupt ne passe aucune information à la méthode ISR qui est appelé. En conséquence, c'est le nom de la méthode qui est associé avec l'interruption qui permet de savoir quel est le port et qu'elles sont les actions à effectuer.

Si nous supposons monitoré l'ensemble des 13 ports de la carte DUE, nous devrions écire trois fonctions pour chacun des ports :

void OnPressedPort1();
void OnReleasedPort1();
void OnChangedPort1();

et de même pour le port 2, 3, ….

On va profite des objets pour simplifier l'écriture. Pour ce faire, nous allons définir une classe qui va permettre de créer de manière récursive les treizes instances des fonctions de base Pressed corresspondant à l'événement le bouton a été pressé, Released que le bouton a été relâché, Changed que l'état du bouton a changé. Ces fonctions vont simplement appelé pour le bouton si un bouton est présent sur le port donné la méthode virtuelle OnPressed(), (resp. OnReleased() ou OnChanged()) définies dans la classe Button. Nous appelons cette classe _ButtonEvents.

template <int... _Is> class _ButtonEvents;

Ceci déclare la classe ButtonEvents comme étant une classe qui peut-être paramétrée par aucune, une ou plusieurs valeurs entières, c'est pourquoi il y a les après int.

Ainsi, nous pouvons écrire :

  _ButtonEvents<> emptyEvents; 
  _ButtonEvents<0> emptyEvents; 
  _ButtonEvents<0, 1> emptyEvents; 
  _ButtonEvents<0, 1, ..., 10> emptyEvents; 

Maintenant, nous allons utiliser définir un schéma de template récursif.

Nous définissons la classe vide, ie. le noeud terminal qui est vide.

template<> class _ButtonEvents {}; 
template<int First, int... _Rest>
class _ButtonEvents <First, _Rest...> : private _ButtonEvents <_Rest...> 
{
private:
    static Button* mButton;
    static void Pressed();
    static void Released();
    static void Changed() { if (mButton != NULL) mButton->OnChanged(); }
}; 

Et voilà, c'est fait, la définition suivante va créer 3 instances des fonctions Pressed(), Released() etChanged().

    _ButtonEvents<0, 1, 2> EventsFor3Ports;

Le compilateur va développer récursivement :

_ButtonEvents <0, 1, 2> : private _ButtonEvents <1, 2> 
{
private:
    static Button* mButton;
    static void Pressed();
    static void Released();
    static void Changed();
}; 
_ButtonEvents <1, 2> : private _ButtonEvents <2> 
{
private:
    static Button* mButton;
    static void Pressed();
    static void Released();
    static void Changed();
}; 
_ButtonEvents <2> : private _ButtonEvents <> 
{
private:
    static Button* mButton;
    static void Pressed();
    static void Released();
    static void Changed();
}; 

C'est bien, mais il faut ensuite penser à d'une part :

  1. passer comme argument à cette objet l'objet à appeller.
  2. récupérer la fonction.

Pour récupérer les fonctions, nous allons définir les fonctions suivantes :

template<int First, int... _Rest>
class _ButtonEvents <First, _Rest...> : private _ButtonEvents <_Rest...> 
{
private:
    static Button* mButton;
    static void Pressed();
    static void Released();
    static void Changed();
public:
    static auto GetPressed(int thePort) 
    { return thePort == First ? Pressed : _ButtonEvents<_Rest...>::GetPressed(thePort); }
    static auto GetReleased(int thePort) 
    { return thePort == First ? Released : _ButtonEvents<_Rest...>::GetReleased(thePort); }
    static auto GetChanged(int thePort)
    { return thePort == First ? Changed : _ButtonEvents<_Rest...>::GetChanged(thePort); } 
}; 

Le fonctionnement de ces fonctions est le même. Nous passons le numéro de port à la fonction. Soit c'est le même numéro de port et nous renvoyons la référence de la fonction définie dans cette classe, soit au contraire, les fonctions associées à ce numéro de port sont définies dans une classe héritée. Nous appelons donc la fonction de la classe héritée avec ce même numéro de port.

Cependant, il faut garantier la terminaison et définir ces fonctions pour une liste de paramètre vide. Donc nous devons ajouter à la classe finale des fonctions qui retournent un pointeur NULL.

template<> class _ButtonEvents
{
    typedef void (*action_type)();
public:
    static auto GetPressed(int thePort) { return (action_type)NULL; }
    static auto GetReleased(int thePort) { return (action_type)NULL; }     
    static auto GetChanged(int thePort) { return (action_type)NULL; } 
};

Maintenant, chaque fois que nous créeons un bouton, il faut mettre à jour la table des fonctions interruptions pour associer l'objet au port.

Pour ce faire, il suffit de définit une fonction SetupButton(int thePort, Button* theButton) comme suit :

template<int First, int... _Rest>
class _ButtonEvents <First, _Rest...> : private _ButtonEvents <_Rest...> 
{
private:
    static Button* mButton;
    static void Pressed();
    static void Released();
    static void Changed();
public:
    static auto GetPressed(int thePort) 
    { return thePort == First ? Pressed : _ButtonEvents<_Rest...>::GetPressed(thePort); }
    static auto GetReleased(int thePort) 
    { return thePort == First ? Released : _ButtonEvents<_Rest...>::GetReleased(thePort); }
    static auto GetChanged(int thePort)
    { return thePort == First ? Changed : _ButtonEvents<_Rest...>::GetChanged(thePort); } 
    static void SetButton(int thePort, Button* theButton)
    {
        if(thePort == First) 
            mButton = theButton;
        else
            _ButtonEvents<_Rest...>::SetButton(thePort, theButton); 
    }
};

Et bien entendu, il est nécessaire de ne pas oublier SetButton dans la fonction terminale même si elle ne fait rien.

template<> class _ButtonEvents
{
    typedef void (*action_type)();
public:
    static auto GetPressed(int thePort) { return (action_type)NULL; }
    static auto GetReleased(int thePort) { return (action_type)NULL; }     
    static auto GetChanged(int thePort) { return (action_type)NULL; } 
    static void SetButton(int thePort, Button* theButton) {}
};

Que reste-t-il encore à faire ?

    typedef _ButtonEvents<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13> ButtonEvents;

Il est plus facile d'utiliser ButtonEvents que _ButtonEvents<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13>.

class Button
{
private:
...
public:
...
    Button(uint32_t thePortNumber, const char* theName, bool isDebug = false):
      m_PortNumber(thePortNumber), Component(theName, isDebug)
    { 
        pinMode(m_PortNumber, INPUT);
        ButtonEvents::SetButton(m_PortNumber, this);
    }
...
};
    virtual void OnPressed()
    {}
    virtual void OnReleased()
    {}
    virtual void OnChanged()
    {}
template<int First, int... _Rest>
void _ButtonEvents<First, _Rest...>::Pressed() 
{ if(mButton != NULL) mButton->OnPressed(); }
template <int First, int... _Rest>
void _ButtonEvents<First, _Rest...>::Released() 
{ if(mButton != NULL) mButton->OnReleased(); }
template <int First, int... _Rest>
void _ButtonEvents<First, _Rest...>::Changed() 
{ if(mButton != NULL) mButton->OnChanged(); }
template<int First, int... _Rest>
Button* _ButtonEvents<First, _Rest...>::mButton = NULL;

Voilà c'est tout, il suffit de compiler.

Question n°3

Nous considérons les deux événements que nous définissons dans le type enum suivant qui définit trois valeurs PRESSED indiquant que le bouton vient d'être enfoncé et RELEASED indiquant que le bouton est relâché et CHANGED.

    enum button_event { PRESSED, RELEASED, CHANGED };

Nous souhaitons créer une fonction qui active l'interruption ou les interruptions nécessaires pour chacun des événements identifiés par PRESSED, RELEASED et CHANGED

Typiquement, l'action de MonitorEvents(PRESSED) d'activer le moniteur d'interruption suivant :

    attachInterrupt(m_PortNumber, ButtonEvents.GetPressed(m_PortNumber), RISING);

Compléter le code de la classe.

class Button
{
public:
    void MonitorEvents(button_event theEvents)
    {
    ...
    }
}

Code correspondant à la classe Button

#ifndef buttonHPP
#define buttonHPP
#include"Component.hpp"
 
enum ButtonEvent { PRESSED = 0x1, RELEASED = 0x2 };
 
typedef void (*interrupt_type)();
 
class Button;
 
template <int... _Is>
class _ButtonEvents;
template <>
class _ButtonEvents<> {
public:
    static interrupt_type GetPressed(int thePort) { return NULL; } 
    static interrupt_type GetReleased(int thePort) { return NULL; }
    static interrupt_type GetChanged(int thePort) { return NULL; }
    static void SetButton(int thePort, Button* theButton) {}
}; // Empty events
template <int First, int... _Rest>
class _ButtonEvents<First, _Rest...> : private _ButtonEvents<_Rest...> 
{ 
private:
    static const int mPortNumber = First;
    static Button* mButton;
    static void Pressed();
    static void Released();
    static void Changed();
public:
    static interrupt_type GetPressed(int thePort) { return thePort == mPortNumber ? Pressed : _ButtonEvents<_Rest...>::GetPressed(thePort); }
    static interrupt_type GetReleased(int thePort) { return thePort == mPortNumber ? Released : _ButtonEvents<_Rest...>::GetReleased(thePort); }
    static interrupt_type GetChanged(int thePort){ return thePort == mPortNumber ? Changed : _ButtonEvents<_Rest...>::GetChanged(thePort); } 
    static void SetButton(int thePort, Button* theButton);
};
 
typedef _ButtonEvents<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13> ButtonEvents;
 
class Button: public Component
{
private:
    uint32_t m_PortNumber;
public:
    Button(uint32_t thePortNumber, const char* theName, bool isDebug = false):
      m_PortNumber(thePortNumber), Component(theName, isDebug)
    { 
        pinMode(m_PortNumber, INPUT);
        ButtonEvents::SetButton(m_PortNumber, this);
    }
    void MonitorEvents(ButtonEvent anEvent)
    {
      if(anEvent == (PRESSED | RELEASED))
          attachInterrupt(digitalPinToInterrupt(m_PortNumber), 
            ButtonEvents::GetChanged(m_PortNumber), CHANGE);
      else if(anEvent == PRESSED)
      {
          attachInterrupt(digitalPinToInterrupt(m_PortNumber), 
            ButtonEvents::GetPressed(m_PortNumber), RISING);
      }
      else if(anEvent == RELEASED)
          attachInterrupt(digitalPinToInterrupt(m_PortNumber), 
            ButtonEvents::GetReleased(m_PortNumber), FALLING);
    }
    bool IsPressed() { return digitalRead(m_PortNumber) == HIGH; }
    virtual void OnPressed()
    {
      if(IsDebug())
          Serial << name() << "Button is pressed\n";
    }
    virtual void OnReleased()
    {
      if(IsDebug())
          Serial << name() << "Button is released\n";
    }
    virtual void OnChanged()
    {
      if(IsDebug())
          Serial << name() << "Button state has changed\n";
    }
};
template<int First, int... _Rest>
Button* _ButtonEvents<First, _Rest...>::mButton = NULL;
template<int First, int... _Rest>
void _ButtonEvents<First, _Rest...>::Pressed() 
{ if(mButton != NULL) mButton->OnPressed(); }
template <int First, int... _Rest>
void _ButtonEvents<First, _Rest...>::Released() 
{ if(mButton != NULL) mButton->OnReleased(); }
template <int First, int... _Rest>
void _ButtonEvents<First, _Rest...>::Changed() 
{ if(mButton != NULL) mButton->OnChanged(); }
 
template <int First, int... _Rest>
void _ButtonEvents<First, _Rest...>::SetButton(int thePort, Button* theButton)
{
    if(thePort == mPortNumber) 
        mButton = theButton;
    else
        _ButtonEvents<_Rest...>::SetButton(thePort, theButton);
}
 
#endif

Question n°4

Pour définir un bouton qui implante une action quand l'utilisateur appuie sur le bouton, il suffit de surcharger désormais dans une classe dérivée la méthode OnPressed().

Vous pouvez désormais écrire :

class LedButton: Button
{
private:
    Led mLed;
public:
    LedButton(uint32_t thePortNumber, Led& theLed): Button(thePortNumber, "LedButton", false)
    {
        MonitorEvents(PRESSED);
    }
    void OnPressed()
    {
        if(mLed.IsSwitchOn())
            mLed.SwitchOff();
        else
            mLed.SwitchOn();
    }
};

Navigation

Précédent: Jalon 3: Les limites de l'approche synchrone

Suivant: Jalon 5: Ajouter des fonctions supplémentaires