====== Jalon 4: Ajouter à la classe Button la réactivité ======
[[in202:seance_6|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 à :
* ''OnPressed()'' : on appuie sur le bouton.
* ''OnReleased()'' : on relâche le bouton.
* ''OnChanged()'' : soit le bouton a été appuyé, soit le bouton a été rélâché.
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 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
class _ButtonEvents : 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()'' et''Changed()''.
_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 :
- passer comme argument à cette objet l'objet à appeller.
- récupérer la fonction.
Pour récupérer les fonctions, nous allons définir les fonctions suivantes :
template
class _ButtonEvents : 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
class _ButtonEvents : 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 ?
* Définir un alias sur la séquence de 14 valeurs :
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>''.
* Modifier ''Button'' pour indiquer chaque fois que l'on crée un bouton qu'un nouveau bouton est associé au port.
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);
}
...
};
* Ajouter à la classe ''Button'' les méthodes ''OnPressed()'', ''OnReleased()'', ''OnChanged()'' :
virtual void OnPressed()
{}
virtual void OnReleased()
{}
virtual void OnChanged()
{}
* Définir les méthodes ''Pressed'', ''Released'' et ''OnChanged'' de la classe ''_ButtonEvents''. Ces méthodes devront être définies après la définition de la classe ''Button''.
template
void _ButtonEvents::Pressed()
{ if(mButton != NULL) mButton->OnPressed(); }
template
void _ButtonEvents::Released()
{ if(mButton != NULL) mButton->OnReleased(); }
template
void _ButtonEvents::Changed()
{ if(mButton != NULL) mButton->OnChanged(); }
* Indiquer comment on initialise le champ ''mButton'' de la classe ''__ButtonEvents''.
template
Button* _ButtonEvents::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
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
class _ButtonEvents : 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
Button* _ButtonEvents::mButton = NULL;
template
void _ButtonEvents::Pressed()
{ if(mButton != NULL) mButton->OnPressed(); }
template
void _ButtonEvents::Released()
{ if(mButton != NULL) mButton->OnReleased(); }
template
void _ButtonEvents::Changed()
{ if(mButton != NULL) mButton->OnChanged(); }
template
void _ButtonEvents::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: **[[.:jalon_3|Les limites de l'approche synchrone]]
Suivant: ** Jalon 5: **[[.:jalon_5|Ajouter des fonctions supplémentaires]]