Utilisation d'une instance d'une classe dans une autre classe

Bonjour,
Après beaucoup (trop) de temps passé sur Python, je fais un gros retour aux sources et repasse sur Arduino. Et quoi de mieux pour se remettre sur les rails qu'un gros projet d'un niveau démoniaque, non ?

Bon, trêve d'humour, comme je suis un peu (beaucoup) rouillé, je me tourne vers vous. Pour ce projet que je présenterai très certainement une fois terminé, j'ai besoin d'utiliser des petits capteurs tactiles TTP223, couplés à des leds RGB WS2812B (chaque bouton aura sa led attribué, juste au dessus de lui dans une impression 3D, pour créer une sorte de "bouton interactif").

J'ai comme objectif de créer une machine à état associée à plusieurs type de clique : des cliques longs, des cliques courts, des doubles cliques, etc... Et comme il y a plusieurs boutons qui auront le même principe de fonctionnement, j'ai décidé d'encapsuler la machine à état dans une classe (spoiler, elle s'appelle "ZoneEclairage" :wink:)

Je souhaitai utiliser Toggle pour approfondir mes connaissances sur cette bibliothèque, mais malheureusement il y a une incompatibilité entre celle-ci et FastLED, utilisée pour contrôler les leds RGB. Je suis donc passé à OneButton (qui présente l'avantage d'avoir nativement des fonctions pour gérer les différents types de cliques).

J'aimerai intégrer dans ma classe à la fois la machine à état mais aussi la gestion de la LED RGB associée au bouton ! Donc pour cela j'ai besoin

  1. De trouver comment attacher les différentes fonctions de la classe pour gérer la machine à état aux fonctions de OneButton directement dans la classe et non pas dans le programme;
  2. Cette question en pose une autre : comment créer et utiliser une instance de FastLED et de OneButton (OneButtonTiny dans mon cas) dans la classe !

Je ne sais pas si je suis clair, si ce n'est pas le cas pas de problème, je peux fournir plus d'infos et/ou reformuler ma (que dis-je, mes) questions :slight_smile:

Cordialement
Pandaroux007

Vous déclarez simplement 2 variables d’instance du type souhaité.

Dans le constructeur de votre classe vous passez les paramètres pour les constructeurs de ces instances

Un exemple avec C qui a des variables d’instances des classes A et B

class A {
public:
    A(int number, const char* text) {
        // Constructeur de A
    }
};

class B {
public:
    B(unsigned char x) {
        // Constructeur de B
    }
};

class C {
private:
    A a;
    B b;
public:
    C(int number, const char* text, unsigned char x ) 
        : a(number, text), b(x) { // la liste d’initialisation permet d’instancier les 2 variables 
        // ici le reste du code du constructeur 
    }
};

Vous pourriez envisager que votre classe hérite de OneButton aussi par exemple.

Qu'entends tu par attacher?
Si ta machine à état peut changer d'état sur des temps dépasser, il faudra que tu fasse comme les libraires de gestion de bouton une fonction à appeler à chaque tour de loop.
Tu peux aussi faire un timer dédié et ne rien faire dans la loop, mais tu tombes sur des problèmes eventuels avec d'autre librairie qui utiliserait aussi un timer.

en fait l'incompatibilité sur les enum peut être levée en important "dans le bon sens" les bibliothèques (d'abord Toggle.h puis ensuite FastLED.h

voir cet exemple:

le code
// attention importer Toggle AVANT FastLED sinon il y a conflit sur un enum
#include <Toggle.h>
#include <FastLED.h>

const byte brocheBouton = 3;
const byte brocheBandeau = 2;
const byte nbLeds = 64;
CRGB leds[nbLeds];
Toggle bouton;

void setup() {
  FastLED.addLeds<WS2812B, brocheBandeau, GRB>(leds, nbLeds);
  Serial.begin(115200);
  bouton.begin(brocheBouton);
}

void loop() {
  bouton.poll();
  if (bouton.onPress()) {
    fill_rainbow(leds, nbLeds, 0, 3);
    FastLED.show();
  } else if (bouton.onRelease()) {
    fill_solid(leds, nbLeds, CRGB(0x000000ul));
    FastLED.show();
  }
}

Bonjour et merci pour votre réponse, @J-M-L :slight_smile:
Ça fonctionne bien :wink:

Maintenant une autre problématique s'ouvre, chaque bouton doit avoir une LED RGB WS2812B attribuée. J'aimerai utiliser une seule pin de ma carte car un seul bandeau pour toutes les LEDs, et utiliser l'index de la LED pour les utiliser séparément (FastLED fonctionne avec une sorte de tableau de LEDs donc c'est parfait)

Or, pour faire cela je n'ai besoin que d'un objet CRGB, alors que je déclare autant de classe "ZoneEclairage" qu'il y a de bouton !, j'ai donc envisagé un fichier header qui contiendrai les index de toutes les leds, et passer aux objets de ma classe l'adresse d'un commun objet CRGB et l'index de la led associé au bouton. Est-ce que cette méthode est la bonne ?


Merci pour l'info pour le problème entre les deux bibliothèques ! :+1:

J'ai cherché dans le code de Toggle (FastLED est énorme, trop compliqué pour moi pour l'instant !) le pourquoi du comment cette incompatibilité mais je n'ai pas trouvé... Pourquoi le fait d'importer Toggle avec FastLED résout le bug ?

Cordialement
Pandaroux007

une explication du bug

si on fait les include dans l'autre sens on n'a pas le souci car FastLed définit DONE aussi dans un .h où il est directement utilisé sans être utilisé ailleurs


pour votre problématique il faut que la classe que vous créez (qui pourrait être une sous classe de Toggle) pourrait avoir dans son constructeur une référence au bandeau, la position de la LED dans ce bandeau et la pin du bouton ou alors une référence à la case dans le bandeau

Merci pour l'information concernant le bug ! (c'est vous JM-FRANCE (JM) · GitHub :wink: ?)


C'est à dire, une référence ? Un pointeur ?

Cordialement
Pandaroux007

Non une référence ça se comporte comme la variable elle même, pas la peine de mettre une * devant (mais on pourrait aussi utiliser un pointeur)

Tenez, c'est vite fait de l'écrire :slight_smile:

j'ai un bandeau de 3 LEDs
je crée un tableau avec 3 instances de ma classe superToggle

une instance prend une référence vers sa led, la broche du bouton et la couleur de la LED que l'on veut.

vous verrez la directive using que vous ne connaissez peut être pas, en gros ça veut dire que ma classe fait suivre les appels aux fonctions poll, onPress et onRelease à la classe parent (Toggle)

le code
// attention importer Toggle AVANT FastLED sinon il y a conflit sur un enum
#include <Toggle.h>
#include <FastLED.h>

const byte brocheBandeau = 2;
const byte nbLeds = 3;
CRGB leds[nbLeds];

class superToggle : Toggle {
  public:
    CRGB & led;
    byte brocheBouton;
    uint32_t couleur;
    superToggle(CRGB & led, byte brocheBouton, uint32_t couleur) : Toggle(brocheBouton), led(led), brocheBouton(brocheBouton), couleur(couleur) {}
    void begin() {
      Toggle::begin(brocheBouton);
    }
    void on() {
      led = couleur;
    }
    void off() {
      led = 0x000000;
    }
    using Toggle::poll; // Expose la méthode poll de Toggle
    using Toggle::onPress; // Expose la méthode onPress de Toggle
    using Toggle::onRelease; // Expose la méthode onRelease de Toggle
};

superToggle boutons[] = {
  {leds[0], 3, 0xFF0000},
  {leds[1], 4, 0x00FF00},
  {leds[2], 5, 0x0000FF},
};

void gestionBoutons() {
  bool changement = false;
  for (auto &bouton : boutons) {
    bouton.poll();
    if (bouton.onPress()) {
      bouton.on();
      changement = true;
    } else if (bouton.onRelease()) {
      bouton.off();
      changement = true;
    }
  }
  if (changement) FastLED.show();
}

void setup() {
  FastLED.addLeds<WS2812B, brocheBandeau, GRB>(leds, nbLeds);
  Serial.begin(115200);
  for (auto &b : boutons) b.begin();
}

void loop() {
  gestionBoutons();
}

Bonjour et merci @J-M-L pour cet exemple très complet :wink:
Du coup je vais utiliser une référence vers l'objet CRGB (déclarée en static dans le private de la classe pour n'en déclarer qu'une pour toutes les instances, ça économisera un peu de mémoire) au lieu du pointeur que j'avais initialement prévu.

J'ai un nouveau problème maintenant : j'aimerai attacher des fonctions de ma classe comme callback aux fonctions de clique du bouton (j'utilise OneButton et pas Toggle du coup, je changerai probablement quand le code sera fonctionnel) dans mon constructeur. Seulement, quand je fais ça

bouton.attachClick(ZoneEclairage::etatAllumer1H);

Le compilateur me revoie ceci :

l'argument de type "void (ZoneEclairage::*)()" est incompatible avec le paramètre de type "callbackFunction" (aka "void (*)()")

Et effectivement, je ne peux pas passer un pointeur à une fonction qui attend une adresse !
Alors comment puis-je faire pour contourner ça ?

Merci encore pour votre réponse,
Cordialement
Pandaroux007

On ne peut pas simplement passer une fonction membre d'une classe en paramètre car la fonction doit s'exécuter dans le contexte de l'instance.

On peut faire des choses avec des fonctions avancées du C++ (je suppose que c'est du chinois si je vous dit que vous pourriez "passer une fonction en paramètre qui est une lambda qui capture l'instance et appelle la fonction sous jacente dans l'instance" ou "utiliser std::bind()" qui peut lier des arguments à une fonction ou une méthode, créant ainsi une nouvelle fonction ou méthode avec certains arguments déjà définis.)

Bonjour @J-M-L

Yep, c'est même de l'alien :sweat_smile: Lambda ça me dit quelque chose, j'ai déjà entendu ça en python, mais en c++ ?


Concrètement cela veut dire que ce que je veux faire ne fonctionne pas avec mon niveau de connaissance actuel ?

Cordialement
Pandaroux007

peut-être plus simple pour vous : vous pourriez peut-être donner lors de la définition du callback l'instance sur lequel celui ci doit être appliqué, cela résout le problème du contexte inconnu

dans ce code j'ai une classe B à qui je veux apprendre qu'elle doit faire un callback au sein d'une instance de la classe A.

Pour configurer le callback dans B, je donne en paramètre l'instance de A qui doit recevoir l'appel ansi que la fonction à appeler.

class A;  // Déclaration anticipée de la classe A

// Définition du type de callback en utilisant un pointeur vers une fonction membre d'une classe A
typedef void (A::*TypeCallback)(int);

class B {
  public:
    void attribuerCallback(A* instance, TypeCallback callback) {
      this->instance = instance;
      this->callback = callback;
    }

    void faireQuelqueChose(int valeur) {
      if (callback != nullptr && instance != nullptr) {
        (instance->*callback)(valeur);
      }
    }

  private:
    A* instance = nullptr;
    TypeCallback callback = nullptr;
};

class A {
  public:
    A(const char * nom) : nom(nom) {}

    // Méthode qui sera utilisée comme callback
    void monCallback(int valeur) {
      Serial.print("Callback dans ");
      Serial.print(nom);
      Serial.print(", j'ai reçu la valeur : ");
      Serial.println(valeur);
    }

    // Méthode pour changer le nom
    void changerNom(const char * nouveauNom) {
      nom = nouveauNom;
    }

  private:
    const char * nom;
};

A a1("instance a1"), a2("instance a2");
B b1, b2;

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

  b1.attribuerCallback(&a1, &A::monCallback);
  b2.attribuerCallback(&a2, &A::monCallback);

  b1.faireQuelqueChose(10);
  b2.faireQuelqueChose(20);

  // on change le nom pour montrer que le callback tient bien compte de l'instance telle qu'elle est
  a1.changerNom("nouveau nom instance a1");
  a2.changerNom("nouveau nom instance a2");

  b1.faireQuelqueChose(30);
  b2.faireQuelqueChose(40);
}

void loop() {
}

le moniteur série affichera

Callback dans instance a1, j'ai reçu la valeur : 10
Callback dans instance a2, j'ai reçu la valeur : 20
Callback dans nouveau nom instance a1, j'ai reçu la valeur : 30
Callback dans nouveau nom instance a2, j'ai reçu la valeur : 40

En C++ il faut indiquer l'instance de l'objet qui appelle la fonction membre non static de la classe, il faut donc dans le code pouvoir passer le 'this' qui est un pointeur accessible dans toutes les fonctions non statiques membre d'une classe. this pointe vers la fonction membre (non statique donc) qui est appelée. S'il y a plusieurs instances de la classe, ça permet de savoir quelle instance/objet a appelé la fonction non statique.

Avec la librairie OnbuttonTiny c'est compliqué mais avec la librairie Onbutton normale un F12 montre qu'il y a 2 solutions possibles pour l'évènement

void OneButton::attachClick(callbackFunction newFunction)
void OneButton::attachClick(parameterizedCallbackFunction newFunction, void *parameter)

La seconde est celle prévue pour ce "problème".

Il faudra donc faire ceci

//le cpp
ZoneEclairage::ZoneEclairage(byte pinBouton)
: bouton(pinBouton)
{
  this->bouton.attachClick(&call_etatAllumer1H,this);
}

void ZoneEclairage::call_etatAllumer1H(void *instance) 
{
    ZoneEclairage *cible = (ZoneEclairage*)(instance); 
    cible->etatAllumer1H();
}

void ZoneEclairage::etatAllumer1H()
{
  // do job  
}

avec

//le .h
#ifndef ZoneEclairage_h
#define ZoneEclairage_h
#include <Arduino.h>
#include <OneButton.h>

class ZoneEclairage
{
  private:
    OneButton bouton;
    static void call_etatAllumer1H(void *instance) ;

  public:
    ZoneEclairage(byte pinBouton);

    void etatAllumer1H(void);
};

#endif //ZoneEclairage_h

Explications :
Tu passes à attachClick l'adresse d'une fonction call_TaFonction ET this, le pointeur vers l'instance qui appelle.

quand il y aura évènement la fonction call_etatAllumer1H sera appelée avec en paramètre le pointeur vers l'objet qui a fait l'appel.

ZoneEclairage *cible = (ZoneEclairage*)(instance); 

défini un pointeur vers l'objet qui a appelé la fonction en faisant un cast sur le bon type de pointeur (instance est un pointeur indifini de type void* et (ZoneEclairage*) fait un cast pour en faire un pointeur vers un objet de type ZoneEclairage.
cible->etatAllumer1H(); appelle la bonne fonction. cible étant la bonne instance de la classe et etatAllumer1H son membre qui nous intéresse.

Tout ceci peut s'écrire un peu plus simplement avec une fonction lambda (la même chose mais qui n'a pas de nom)

// le cpp
#include <ZoneEclairage.h>

ZoneEclairage::ZoneEclairage(byte pinBouton)
: bouton(pinBouton)
{
  this->bouton.attachClick([](void *instance) {
      ZoneEclairage * cible= (ZoneEclairage *)instance;
      cible->etatAllumer1H();
  }, this);
  
}

void ZoneEclairage::etatAllumer1H()
{
  // do job  
}
 le .h
#ifndef ZoneEclairage_h
#define ZoneEclairage_h
#include <Arduino.h>
#include <OneButton.h>

class ZoneEclairage
{
  private:
    OneButton bouton;
  // ici plus besoin de définir une fonction statique intermédiaire, la lambda le fait sans lui donner de nom

  public:
    ZoneEclairage(byte pinBouton);

    void etatAllumer1H(void);
};

#endif //ZoneEclairage_h

Bonjour @petitours et merci pour cette réponse !
J'ai lu le code que vous proposiez (j'ai eu un peu de mal, ce n'est pas de toute simplicité !), et je n'ai plus d'erreur maintenant. J'utilise la solution avec la fonction lambda.


Dernier problème, après j'arrête promis :slight_smile:
J'ai besoin d'appliquer une couleur en RGB à une LED précisément du bandeau. ça, y'a pas de problème, puis que je passe directement en paramètre lors de la déclaration de l'instance de ma classe l'index de la led sur le bandeau.

Le problème vient du fait que quand je fais ceci dans une des fonctions de ma classe

leds[indexLed].setRGB(this->valeurActuelleLumLed,0,0);
FastLED.show();

Le compilo me retourne une erreur :

l'expression doit avoir un type classe mais elle a le type "uint8_t"

Peut-être est-ce dû au fait que j’utilise une référence au lieu d'un objet CRGB directement ?
Je n'en sais trop rien.

En espérant que vous pourrez m'éclairer,
Cordialement
Pandaroux007

faudrait tout le code..

ça marche, le voici :

La fonction pour utiliser les LEDs
// définition des fonctions de gestion de la led RGB
void ZoneEclairage::ledClignoterDoucement(void)
{
  static uint32_t tempsCourantMillis = millis();
  static uint32_t tempsPrecedentMillis = 0;
  if((tempsCourantMillis - tempsPrecedentMillis) >= intervalUpdateLumLed)
  {
    if (this->sensUpdateValeurLumLed == incrementationLumLed)
    {
      this->valeurActuelleLumLed = this->valeurActuelleLumLed + 5;
      if (this->valeurActuelleLumLed >= 255) {
        this->valeurActuelleLumLed = 255;
        this->sensUpdateValeurLumLed = decrementationLumLed;
      }
    }
    else
    {
      this->valeurActuelleLumLed = this->valeurActuelleLumLed - 5;
      if (this->valeurActuelleLumLed <= 0)
      {
        this->valeurActuelleLumLed = 0;
        this->sensUpdateValeurLumLed = incrementationLumLed;
      }
    }
    leds[indexLed].setRGB(this->valeurActuelleLumLed,0,0);
    FastLED.show();
    tempsPrecedentMillis = tempsCourantMillis;
  }
}
Le code du constructeur
// définition du constructeur
ZoneEclairage::ZoneEclairage(byte pinBouton, CRGB & leds, byte indexLed)
: bouton(pinBouton), leds(leds)
{
  this->pinBouton = pinBouton;
  this->indexLed = indexLed;
  this->bouton.attachClick([](void *instance) {
      ZoneEclairage * cible= (ZoneEclairage *)instance;
      cible->etatAllumer1H();
  }, this);
}
Et enfin le contenu du .h
#ifndef ZoneEclairage_h
#define ZoneEclairage_h
#include <Arduino.h>
#include <FastLED.h>
#include <OneButton.h>

#define incrementationLumLed true
#define decrementationLumLed false
#define intervalUpdateLumLed 30ul

class ZoneEclairage
{
  private:
    // led
    CRGB & leds;
    byte indexLed;
    // bouton
    byte pinBouton;
    OneButton bouton;
    // machine à état
    enum {
      REPOS,
      ALLUME_1H,
      ALLUME_VERS_REPOS,
      ALLUME
    } etats = REPOS;
    // variables
    bool sensUpdateValeurLumLed = incrementationLumLed;
    uint8_t valeurActuelleLumLed = 0;

  public:
    ZoneEclairage(byte pinBouton, CRGB & leds, byte pinLed);
    void update(void);
    // fonctions de la machine à état
    void etatAllumer1H(void);
    void etatAllumer(void);
    void etatRepos(void);
    void etatAllumeVersRepos(void);
    // fonction de gestion de la led RGB
    void ledEteindreCompletement(void);
    void ledAllumerCompletement(void);
    void ledClignoterDoucement(void);
    void ledClignoterRapidement(void);
};

#endif //ZoneEclairage_h

Pour l'instant tout est en écriture, il n'y a rien de fonctionnel.
Désolé si c'est assez peu digeste avec tous les this-> inutiles...

si vous ne donnez pas tout le code (avec la loop et le setup) donnez nous tout le message d'erreur


si vous mettez des variables statiques dans votre classe, elles sont partagées par toutes les instances.

➜ si vous voulez que chaque led ait sa propre tempo, il faut mettre tempsPrecedentMillis en variable d'instance. Quand à tempsCourantMillis il n'a pas besoin d'être statique, c'est juste une variable locale à la fonction.

Bonjour @J-M-L (et les autres qui passeront par ici :wink:)
Désolé pour le retard de ma réponse et merci encore pour la votre !
J'ai décidé de m'y prendre autrement pour faire ce projet, là j'ai essayé de tout coder du premier coup en intégrant tout. Or, qui dit tout intégrer du premier coup dit plein de sources de bug possible !

Donc, j'ai commencé par coder la machine à état et sa gestion dans la classe ZoneEclairage. Le programme est fonctionnel, je l'ai mis sur GitHub pour que ce soit plus simple pour vous et moi de suivre les changements.

Le programme actuel

Voici déjà le code du header

ZoneEclairage.h
#ifndef ZoneEclairage_h
#define ZoneEclairage_h
#include <Arduino.h> // on définit Arduino ici donc on ne le redéclare pas dans le cpp
#include <OneButton.h> // on utilise OneButton et pas OneButtonTiny pour les callbacks dans la classe elle-même
// #include <FastLED.h>

#define RELAIS_ON true
#define RELAIS_OFF false

/* #define TEMPS_FONCTIONNEMENT_SANS_RAPPEL_COURT 3600000ul // 1h, 60 minutes × 60s × 1000ms. Temps avant que l'éclairage s'éteigne tout seul en mode 1H.
#define TEMPS_FONCTIONNEMENT_SANS_RAPPEL_LONG 120000ul // 5h, 5 × 60 minutes × 60s × 1000ms. Temps avant que l'éclairage s'éteigne tout seul en mode long.
#define TEMPS_AVANT_EXTENCTION 300000ul // 5mn × 60 × 1000ms. Temps ou l'on attend un nouveau clique sur le bouton pour relancer le minuteur. */

// temps courts pour les testes du montage !
#define TEMPS_FONCTIONNEMENT_SANS_RAPPEL_COURT 30000ul // 30 secondes
#define TEMPS_FONCTIONNEMENT_SANS_RAPPEL_LONG 60000 // 1 minutes
#define TEMPS_AVANT_EXTENCTION 10000 // 10s

class ZoneEclairage
{
  private:
    // déclaration de l'instance du bouton
    OneButton bouton;
    // minuteur d'extinction
    uint32_t tempsPrecedentClique;
    // machine à état
    enum {
      ALLUME_COURT,
      ALLUME_LONG,
      ALLUME_VERS_REPOS,
      REPOS
    } etats = REPOS;
    // fonctions et pin gestion relais
    void setRelais(bool);
    byte pinRelais;
    bool etatCourantRelais;
    // fonction de gestion des événements du bouton
    void checkEventClic(void);
    void checkEventClicLong(void);

  public:
    ZoneEclairage(byte, byte); // Constructeur
    void update(void); // fonction a appeller à chaque loop, pour gérer le btn et la machine à état
};

#endif //ZoneEclairage_h

... le code du fichier cpp...

ZoneEclairage.cpp
#include <ZoneEclairage.h>

// définition du constructeur
ZoneEclairage::ZoneEclairage(byte pinBouton, byte pinRelais)
: bouton(pinBouton, false /*bp actif sur HIGH puisque pas de PULLUP (LOW par defaut)*/)
{
  // on met la pin du relais en sortie et on enregistre la pin en question
  this->pinRelais = pinRelais;
  pinMode(this->pinRelais, OUTPUT);
  // on attache le clique à la gestion de l'événement,
  // c'est à dire que fait-on dans quel état (utilisation fonction lambda)
  this->bouton.attachClick([](void *instance) {
      ZoneEclairage * cible= (ZoneEclairage *)instance;
      cible->checkEventClic();
  }, this);
  // et on fait pareil pour le clique long!
  this->bouton.attachLongPressStart([](void *instance) {
      ZoneEclairage * cible= (ZoneEclairage *)instance;
      cible->checkEventClicLong();
  }, this);
}

// définition des fonctions de gestion des événements pour les transitions
void ZoneEclairage::checkEventClic() // fonction appelée quand un clique simple est effectué
{
  Serial.print(F("Clique ! >> "));
  switch (etats)
  {
  case ZoneEclairage::ALLUME_COURT: Serial.println(F("On était en ALLUME_COURT, on ne fait rien")); break; // Si la lampe est déjà allumée, alors on ne fait rien, on sort...
  case ZoneEclairage::ALLUME_LONG: Serial.println(F("On était en ALLUME_LONG, on ne fait rien")); break; // pareil.
  case ZoneEclairage::REPOS: // Si on était au repos, alors on passe en mode allumé court (géré dans update)
    Serial.println(F("On était au REPOS, on passe en mode ALLUME_COURT"));
    tempsPrecedentClique = millis(); // on stocke le temps au moment du clique
    this->etats = ZoneEclairage::ALLUME_COURT; // on attribue le nouvel état
    break;
  case ZoneEclairage::ALLUME_VERS_REPOS: // Si on était au repos, alors on passe en mode allumé court (géré dans update)
    Serial.println(F("On était en ALLUME_VERS_REPOS, on repasse en mode ALLUME_COURT"));
    tempsPrecedentClique = millis(); // on stocke le temps au moment du clique
    this->etats = ZoneEclairage::ALLUME_COURT; // on attribue le nouvel état
    break;
  default: // Si état inconnu, bah on le print et on sort.
    Serial.println(F("Erreur dans switch/case checkEventClic !"));
    break;
  }
}

void ZoneEclairage::checkEventClicLong() // fonction appelée quand un clique long est effectué
{
  Serial.print(F("Clique Long ! >> "));
  switch (etats)
  {
  case ZoneEclairage::ALLUME_COURT: // si on était allumé sur un temps court,
    Serial.println(F("On était en ALLUME_COURT, on passe en mode REPOS"));
    this->etats = ZoneEclairage::REPOS; // bah on éteint tout.
    break;
  case ZoneEclairage::ALLUME_LONG: // si on était en mode allumé longtemps,
    Serial.println(F("On était au ALLUME_LONG, on bascule en mode REPOS"));
    this->etats = ZoneEclairage::REPOS; // alors on éteint tout en passant en mode REPOS
    break;
  case ZoneEclairage::REPOS: // si on était tout éteint,
    Serial.println(F("On était au REPOS, on bascule en mode ALLUME_LONG"));
    this->etats = ZoneEclairage::ALLUME_LONG; // alors on passe en mode allumé longtemps
    break;
  case ZoneEclairage::ALLUME_VERS_REPOS: // si on était sur le point de s'éteindre alors que l'on était allumé,
    Serial.println(F("On était en ALLUME_VERS_REPOS, on repasse en mode ALLUME_COURT"));
    tempsPrecedentClique = millis(); // on stocke le temps au moment du clique
    this->etats = ZoneEclairage::ALLUME_COURT; // alors on relance un temps court
    break;
  default: // Si état inconnu, bah on le print et on sort.
    Serial.println(F("Erreur dans switch/case checkEventClicLong !"));
    break;
  }
}

void ZoneEclairage::update()
{
  bouton.tick(); // on met à jour l'état du bouton
  // gestion relais
  switch (etats)
  {
  case ZoneEclairage::REPOS:
    setRelais(RELAIS_OFF); // on éteint le relais
    break;
  case ZoneEclairage::ALLUME_COURT:
    setRelais(RELAIS_ON); // on allume le relais
    // gestion minuteur
    if((millis() - tempsPrecedentClique) >= (TEMPS_FONCTIONNEMENT_SANS_RAPPEL_COURT - TEMPS_AVANT_EXTENCTION))
    {
      Serial.println(F("Fin proche du temps court !"));
      this->etats = ZoneEclairage::ALLUME_VERS_REPOS;
      break;
    }
    break;
  case ZoneEclairage::ALLUME_VERS_REPOS:
    setRelais(RELAIS_ON); // on allume le relais
    // gestion minuteur
    if((millis() - tempsPrecedentClique) >= (TEMPS_FONCTIONNEMENT_SANS_RAPPEL_COURT)) // Si le temps "long" est écoulé,
    {
      Serial.println(F("Fin du temps court, on passe en mode REPOS!"));
      this->etats = ZoneEclairage::REPOS; // alors on passe en mode repos
      break;
    }
    break;
  case ZoneEclairage::ALLUME_LONG:
    setRelais(RELAIS_ON); // on allume le relais
    // gestion minuteur
    if((millis() - tempsPrecedentClique) >= TEMPS_FONCTIONNEMENT_SANS_RAPPEL_LONG) // si le temps long est passé
    {
      Serial.println(F("Fin du temps long, on passe en mode REPOS!"));
      this->etats = ZoneEclairage::REPOS; // alors on éteint tout (ouais là on s'embête pas à faire un rappel, le temps est suffisament long)
      break;
    }
    break;
  default:
    Serial.println(F("État inconnu dans update! Mode REPOS par défaut"));
    // setRelais(RELAIS_OFF); // pas besoin ici puis qu'au prochain tour de loop on verra qu'on est en REPOS et on coupera le relais et la led
    etats = REPOS;
    break;
  }
}

// Comme on utilise souvent la gestion de l'état du relais, une petite fonction ne mange pas de pain ;)
void ZoneEclairage::setRelais(bool etatSouhaite)
{
  if(etatSouhaite != etatCourantRelais)
  {
    etatCourantRelais = etatSouhaite;
    digitalWrite(pinRelais, etatSouhaite); // on applique les changements, puis on le print à la ligne suivante
    Serial.print(F("Relais défini sur ")); Serial.println(etatSouhaite);
  }
}

Plein de print, désolé c'est pour le debug - ça sera viré après :slight_smile:

... et le code du fichier principal !

main.cpp
/*
lib_deps = 
	; Dlloydev/Toggle@^3.1.8
	fastled/FastLED@^3.6.0
	mathertel/OneButton@^2.5.0
	; CMB27/ModbusRTUSlave@^2.0.5
*/

#include <Arduino.h>
// fichiers programmes
#include "ZoneEclairage.h"
#include "definitions.h"

ZoneEclairage eclairageSpotGarage(PIN_BOUTON_GARAGE, PIN_RELAIS_GARAGE);
ZoneEclairage eclairageSpotVelo(PIN_BOUTON_VELO, PIN_RELAIS_VELO);
ZoneEclairage eclairageSpotGuirlande(PIN_BOUTON_GUIRLANDE, PIN_RELAIS_GUIRLANDE);

void setup(void)
{
  // Initialisation du moniteur série
  Serial.begin(115200);
  Serial.println(F("Démarrage!\n-------------------------------------------------------------------"));
  Serial.print(F("Temps de fonctionnement d'une lampe en mode court : ")); Serial.print(TEMPS_FONCTIONNEMENT_SANS_RAPPEL_COURT); Serial.println(F("ms"));
  Serial.print(F("Temps de fonctionnement d'une lampe en mode long  : ")); Serial.print(TEMPS_FONCTIONNEMENT_SANS_RAPPEL_LONG); Serial.println(F("ms"));
  delay(1000); // stop 1s
}

void loop(void)
{
  eclairageSpotGarage.update();
  eclairageSpotVelo.update();
  eclairageSpotGuirlande.update();
}

Vos conseils sur la manière de programmer par machine à état m'ont été bien utiles, notamment le fait de faire un schéma fonctionnel des états et transitions :wink:


Maintenant, je peux m'attaquer à la gestion des LEDs. Et cela ne me semble pas gagné d'avance, vu les tests que j'ai réalisé (dont je parlerai quand j'aurai du temps de disponible ce qui n'est pas le cas maintenant :slight_smile:) - FastLED semble utiliser des pins autres que celle spécifiées pour le ruban de LED ?!

J'ai un buzzer branché sur la pin 13, et il bip légèrement quand je tente d'ajouter la gestion simple des couleurs par état. De plus, malgré l'utilisation d'une référence vers une case spécifique du tableau de led, c'est toujours la même qui s'allume, quelque soit le bouton appuyé!

Le code de mes essais
fichier header
#ifndef ZoneEclairage_h
#define ZoneEclairage_h
#include <Arduino.h> // on définit Arduino ici donc on ne le redéclare pas dans le cpp
#include <OneButton.h> // on utilise OneButton et pas OneButtonTiny pour les callbacks dans la classe elle-même
#include <FastLED.h>

#define RELAIS_ON true
#define RELAIS_OFF false

/* #define TEMPS_FONCTIONNEMENT_SANS_RAPPEL_COURT 3600000ul // 1h, 60 minutes × 60s × 1000ms. Temps avant que l'éclairage s'éteigne tout seul en mode 1H.
#define TEMPS_FONCTIONNEMENT_SANS_RAPPEL_LONG 120000ul // 5h, 5 × 60 minutes × 60s × 1000ms. Temps avant que l'éclairage s'éteigne tout seul en mode long.
#define TEMPS_AVANT_EXTENCTION 300000ul // 5mn × 60 × 1000ms. Temps ou l'on attend un nouveau clique sur le bouton pour relancer le minuteur. */

// temps courts pour les testes du montage !
#define TEMPS_FONCTIONNEMENT_SANS_RAPPEL_COURT 30000ul // 30 secondes
#define TEMPS_FONCTIONNEMENT_SANS_RAPPEL_LONG 60000 // 1 minutes
#define TEMPS_AVANT_EXTENCTION 10000 // 10s

class ZoneEclairage
{
  private:
    // déclaration de l'instance du bouton et de la led
    OneButton bouton;
    CRGB & led;
    // minuteur d'extinction
    uint32_t tempsPrecedentClique;
    // machine à état
    enum {
      ALLUME_COURT,
      ALLUME_LONG,
      ALLUME_VERS_REPOS,
      REPOS
    } etats = REPOS;
    // fonctions et pin gestion relais
    void setRelais(bool);
    byte pinRelais;
    bool etatCourantRelais;
    // fonction de gestion des événements du bouton
    void checkEventClic(void);
    void checkEventClicLong(void);

  public:
    ZoneEclairage(byte, byte, CRGB &); // Constructeur
    void update(void); // fonction a appeller à chaque loop, pour gérer le btn et la machine à état
};

#endif //ZoneEclairage_h
fichier cpp
#include <ZoneEclairage.h>

// définition du constructeur
ZoneEclairage::ZoneEclairage(byte pinBouton, byte pinRelais, CRGB & led)
: bouton(pinBouton, false /*bp actif sur HIGH puisque pas de PULLUP (LOW par defaut)*/),
led(led)
{
  // on met la pin du relais en sortie et on enregistre la pin en question
  this->pinRelais = pinRelais;
  pinMode(this->pinRelais, OUTPUT);
  // on attache le clique à la gestion de l'événement,
  // c'est à dire que fait-on dans quel état (utilisation fonction lambda)
  this->bouton.attachClick([](void *instance) {
      ZoneEclairage * cible= (ZoneEclairage *)instance;
      cible->checkEventClic();
  }, this);
  // et on fait pareil pour le clique long!
  this->bouton.attachLongPressStart([](void *instance) {
      ZoneEclairage * cible= (ZoneEclairage *)instance;
      cible->checkEventClicLong();
  }, this);
}

// définition des fonctions de gestion des événements pour les transitions
void ZoneEclairage::checkEventClic() // fonction appelée quand un clique simple est effectué
{
  Serial.print(F("Clique ! >> "));
  switch (etats)
  {
  case ZoneEclairage::ALLUME_COURT: Serial.println(F("On était en ALLUME_COURT, on ne fait rien")); break; // Si la lampe est déjà allumée, alors on ne fait rien, on sort...
  case ZoneEclairage::ALLUME_LONG: Serial.println(F("On était en ALLUME_LONG, on ne fait rien")); break; // pareil.
  case ZoneEclairage::REPOS: // Si on était au repos, alors on passe en mode allumé court (géré dans update)
    Serial.println(F("On était au REPOS, on passe en mode ALLUME_COURT"));
    tempsPrecedentClique = millis(); // on stocke le temps au moment du clique
    this->etats = ZoneEclairage::ALLUME_COURT; // on attribue le nouvel état
    break;
  case ZoneEclairage::ALLUME_VERS_REPOS: // Si on était au repos, alors on passe en mode allumé court (géré dans update)
    Serial.println(F("On était en ALLUME_VERS_REPOS, on repasse en mode ALLUME_COURT"));
    tempsPrecedentClique = millis(); // on stocke le temps au moment du clique
    this->etats = ZoneEclairage::ALLUME_COURT; // on attribue le nouvel état
    break;
  default: // Si état inconnu, bah on le print et on sort.
    Serial.println(F("Erreur dans switch/case checkEventClic !"));
    break;
  }
}

void ZoneEclairage::checkEventClicLong() // fonction appelée quand un clique long est effectué
{
  Serial.print(F("Clique Long ! >> "));
  switch (etats)
  {
  case ZoneEclairage::ALLUME_COURT: // si on était allumé sur un temps court,
    Serial.println(F("On était en ALLUME_COURT, on passe en mode REPOS"));
    this->etats = ZoneEclairage::REPOS; // bah on éteint tout.
    break;
  case ZoneEclairage::ALLUME_LONG: // si on était en mode allumé longtemps,
    Serial.println(F("On était au ALLUME_LONG, on bascule en mode REPOS"));
    this->etats = ZoneEclairage::REPOS; // alors on éteint tout en passant en mode REPOS
    break;
  case ZoneEclairage::REPOS: // si on était tout éteint,
    Serial.println(F("On était au REPOS, on bascule en mode ALLUME_LONG"));
    this->etats = ZoneEclairage::ALLUME_LONG; // alors on passe en mode allumé longtemps
    break;
  case ZoneEclairage::ALLUME_VERS_REPOS: // si on était sur le point de s'éteindre alors que l'on était allumé,
    Serial.println(F("On était en ALLUME_VERS_REPOS, on repasse en mode ALLUME_COURT"));
    tempsPrecedentClique = millis(); // on stocke le temps au moment du clique
    this->etats = ZoneEclairage::ALLUME_COURT; // alors on relance un temps court
    break;
  default: // Si état inconnu, bah on le print et on sort.
    Serial.println(F("Erreur dans switch/case checkEventClicLong !"));
    break;
  }
}

void ZoneEclairage::update()
{
  bouton.tick(); // on met à jour l'état du bouton
  // gestion relais
  switch (etats)
  {
  case ZoneEclairage::REPOS:
    led.setRGB(0, 0, 0);
    setRelais(RELAIS_OFF); // on éteint le relais
    break;
  case ZoneEclairage::ALLUME_COURT:
    setRelais(RELAIS_ON); // on allume le relais
    led.setRGB(255, 0, 0);
    // gestion minuteur
    if((millis() - tempsPrecedentClique) >= (TEMPS_FONCTIONNEMENT_SANS_RAPPEL_COURT - TEMPS_AVANT_EXTENCTION))
    {
      Serial.println(F("Fin proche du temps court !"));
      this->etats = ZoneEclairage::ALLUME_VERS_REPOS;
      break;
    }
    break;
  case ZoneEclairage::ALLUME_VERS_REPOS:
    setRelais(RELAIS_ON); // on allume le relais
    led.setRGB(0, 255, 0);
    // gestion minuteur
    if((millis() - tempsPrecedentClique) >= (TEMPS_FONCTIONNEMENT_SANS_RAPPEL_COURT)) // Si le temps "long" est écoulé,
    {
      Serial.println(F("Fin du temps court, on passe en mode REPOS!"));
      this->etats = ZoneEclairage::REPOS; // alors on passe en mode repos
      break;
    }
    break;
  case ZoneEclairage::ALLUME_LONG:
    setRelais(RELAIS_ON); // on allume le relais
    led.setRGB(0, 0, 255);
    // gestion minuteur
    if((millis() - tempsPrecedentClique) >= TEMPS_FONCTIONNEMENT_SANS_RAPPEL_LONG) // si le temps long est passé
    {
      Serial.println(F("Fin du temps long, on passe en mode REPOS!"));
      this->etats = ZoneEclairage::REPOS; // alors on éteint tout (ouais là on s'embête pas à faire un rappel, le temps est suffisament long)
      break;
    }
    break;
  default:
    Serial.println(F("État inconnu dans update! Mode REPOS par défaut"));
    // setRelais(RELAIS_OFF); // pas besoin ici puis qu'au prochain tour de loop on verra qu'on est en REPOS et on coupera le relais et la led
    etats = REPOS;
    break;
  }

  FastLED.show(); // on affiche les changement sur la led
}

// Comme on utilise souvent la gestion de l'état du relais, une petite fonction ne mange pas de pain ;)
void ZoneEclairage::setRelais(bool etatSouhaite)
{
  if(etatSouhaite != etatCourantRelais)
  {
    etatCourantRelais = etatSouhaite;
    digitalWrite(pinRelais, etatSouhaite); // on applique les changements, puis on le print à la ligne suivante
    Serial.print(F("Relais défini sur ")); Serial.println(etatSouhaite);
  }
}
fichier principal
/*
lib_deps = 
	; Dlloydev/Toggle@^3.1.8
	fastled/FastLED@^3.6.0
	mathertel/OneButton@^2.5.0
	; CMB27/ModbusRTUSlave@^2.0.5
*/

#include <Arduino.h>
// fichiers programmes
#include "ZoneEclairage.h"
#include "definitions.h"

CRGB leds[NBR_LEDS];

ZoneEclairage eclairageSpotGarage(PIN_BOUTON_GARAGE, PIN_RELAIS_GARAGE, leds[INDEX_LED_GARAGE]);
ZoneEclairage eclairageSpotVelo(PIN_BOUTON_VELO, PIN_RELAIS_VELO, leds[INDEX_LED_VELO]);
ZoneEclairage eclairageSpotGuirlande(PIN_BOUTON_GUIRLANDE, PIN_RELAIS_GUIRLANDE, leds[INDEX_LED_GUIRLANDE]);

void setup(void)
{
  // Initialisation du moniteur série
  Serial.begin(115200);
  Serial.println(F("Démarrage!\n-------------------------------------------------------------------"));
  Serial.print(F("Temps de fonctionnement d'une lampe en mode court : ")); Serial.print(TEMPS_FONCTIONNEMENT_SANS_RAPPEL_COURT); Serial.println(F("ms"));
  Serial.print(F("Temps de fonctionnement d'une lampe en mode long  : ")); Serial.print(TEMPS_FONCTIONNEMENT_SANS_RAPPEL_LONG); Serial.println(F("ms"));
  // initialisation des leds WS2812B
  FastLED.addLeds<WS2812B, PIN_LEDS, GRB /*RGB par défaut, ce qui inversait le vert et le rouge sur mon bandeau!*/>(leds, NBR_LEDS);
  Serial.println(F("LEDs WS2812B initialisées!\n"));
  delay(1000); // stop 1s
}

void loop(void)
{
  eclairageSpotGarage.update();
  eclairageSpotVelo.update();
  eclairageSpotGuirlande.update();
}

Pour l'instant ça fait n'import quoi, au début l'un des relais s'allume et la première led du bandeau aussi (en bleu), le bouton ne semble rien affecter, tandis que les autres fonctionnent normalement. Puis, après un court laps de temps (1 minute environ ?), la led RGB s'eteind, le relais aussi, et il ne réajit toujours pas au bouton. Les autres relais et leurs boutons fonctionnent, et leurs LEDs RGB associées ne s'allument pas!

Je posterai le schéma de mon montage de test dès que possible, je suis en déplacement jusqu'à lundi au moins.

En esperant ne pas vous avoir dégouté de m'aider sur ce projet avec mon pavé :slight_smile:
Cordialement,
Pandaroux007

Sur l'Arduino Uno, les broches 11, 12 et 13 sont utilisées pour l'interface SPI, avec la broche 13 étant la broche SCK (Serial Clock) qui est utilisée pour synchroniser les transferts de données sur le bus SPI.

Si vous utilisez FastLed avec le SPI hardware, il va effectivement utiliser cette pin car c'est plus rapide avec certains type de LEDs

Bonjour @J-M-L,
Merci pour votre réponse, mais je ne comprends pas pourquoi la SPI viendrait s'activer alors que j'utilise des WS2812B - ces LEDs n'utilisent qu'un seul fils, donc même si je ne connais pas le protocole qu'elles utilisent, j'en déduis que ce n'est pas de la SPI.


Je viens de finir le câblage :

Câblage

(désolé, c'est du fritzing, je ferai un vrai schéma quand j'aurai le temps :woozy_face:)


@J-M-L Avez vous une idée de ce qui peut se passer dans mon programme pour causer cela

J'ai beau chercher je ne trouve aucune explication!

Cordialement
Pandaroux007