Aller au contenu

Logique d'automatisation

Assemblons tout : le capteur décide, le ventilateur tourne, le portail commande.

1. État et paramètres

Au début de src/main.cpp :

#include <Preferences.h>

static const int FAN_PIN = 4;

enum class FilterMode : uint8_t { Auto, On, Off };
static FilterMode g_mode      = FilterMode::Auto;
static int32_t    g_threshold = 150;   // indice VOC de déclenchement
static bool       g_fanOn     = false;

static Preferences s_prefs;

Preferences (NVS) — mémoire non-volatile de l'ESP32 : mode et seuil survivent à un redémarrage.

2. Commande du ventilateur

Centralisez tout allumage et éteignage dans une seule fonction setFan. Elle prend un argument on — l'état désiré : true = allumer, false = éteindre. Plus loin dans le code, on appelera toujours setFan(true) / setFan(false), et elle fait toute la routine : tire la broche, mémorise l'état et signale au portail.

static void setFan(bool on) {      // on — argument: true = allumer, false = éteindre
    if (g_fanOn == on) return;     // déjà dans l'état désiré — ne faire rien
    g_fanOn = on;                  // mémoriser le nouvel état en variable globale
    digitalWrite(FAN_PIN, on ? HIGH : LOW);  // allumer/éteindre physiquement la clé du ventilateur

    // Signaler au noyau l'état : fanOn[0] — champ du dictionnaire en télémétrie
    // (apparu de hasFan = true; [0] — notre seule unité, comme chapitre 5).
    // De là il ira au cloud et à la cellule « Ventilateur » de la fiche.
    s_link.telemetry.fanOn[0] = on;

    // Changement d'état — raison d'envoyer la télémétrie tout de suite, pas attendre la période.
    s_link.publishTelemetryNow();
}

publishTelemetryNow() fait la réponse instantanée : on clique sur le portail — en une seconde la fiche affiche l'état confirmé. Exactement confirmé : le portail iDryer ne « devine » jamais l'état, il affiche ce que l'appareil a vraiment envoyé.

3. Automatisation avec hystérésis

Si on allume le ventilateur juste sur le seuil, il va se mettre à vibrer allumé/éteint près du seuil. On soigne ça avec un écart :

static void tickAutoLogic() {
    if (g_mode == FilterMode::On)  { setFan(true);  return; }
    if (g_mode == FilterMode::Off) { setFan(false); return; }

    // Auto: allumage au seuil, éteignage 20 points au-dessous.
    if (g_vocIndex < 0) return;                      // capteur est muet encore
    if (!g_fanOn && g_vocIndex >= g_threshold)      setFan(true);
    if ( g_fanOn && g_vocIndex <= g_threshold - 20) setFan(false);
}

On appelle tickAutoLogic() là où on lit le capteur — dans loop() par minuteur de seconde. C'est ce même loop() du chapitre 5, on y ajoute une ligne. Complet, il ressemble à :

void loop() {
    s_link.loop();                        // réseau, télémétrie, commandes — toujours d'abord

    uint32_t now = millis();
    if (now - s_lastReadMs >= 1000) {     // une fois par seconde:
        s_lastReadMs = now;
        readVocSensor();                  //   lire VOC (chapitre 5)
        tickAutoLogic();                  //   et aussitôt décider du ventilateur
    }
}

L'ordre à l'intérieur du bloc de seconde n'est pas du hasard : d'abord lecture du capteur, puis décision selon elle.

4. Callbacks depuis le portail

Exactement les fonctions qu'on a promises au chapitre 6 :

static void onModeSelected(const char* opt) {
    if      (strcmp(opt, "auto") == 0) g_mode = FilterMode::Auto;
    else if (strcmp(opt, "on")   == 0) g_mode = FilterMode::On;
    else                               g_mode = FilterMode::Off;
    s_prefs.putUChar("mode", (uint8_t)g_mode);
    tickAutoLogic();   // appliquer tout de suite, pas attendre le prochain tick
}

static void onThresholdChanged(float v) {
    g_threshold = (int32_t)v;
    s_prefs.putInt("thr", g_threshold);
}

Ces fonctions remplacent les stubs du chapitre 6 — supprimez les versions vides.

Remarquez ce qu'il n'y a pas dans ce code : parsing MQTT, topics, commandes JSON. L'utilisateur a sélectionné on dans la liste sur le portail → le noyau a reçu la commande, l'a vérifiée et appelé onModeSelected("on"). Toute la mécanique de transport — c'est du noyau.

5. setup() final

Reste à ajouter deux choses : chargement des paramètres sauvegardés depuis NVS (au début, pour que la logique travaille avec) et configuration de la broche du ventilateur. Le setup() complet après ce chapitre ressemble à :

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

    // Paramètres depuis NVS : ce que l'utilisateur a sélectionné auparavant.
    s_prefs.begin("filter");   // ouvrir l'espace de noms "filter" en NVS
    g_mode      = (FilterMode)s_prefs.getUChar("mode", (uint8_t)FilterMode::Auto);
    g_threshold = s_prefs.getInt("thr", 150);
    // Deuxièmes arguments getUChar/getInt — valeurs par défaut : reviennent
    // au tout premier démarrage, quand il n'y a encore rien en NVS.

    pinMode(FAN_PIN, OUTPUT);  // broche de la clé du ventilateur — en sortie

    s_link.begin();
    initVocSensor();

    // Télémétrie: votre champ vocIndex (chapitre 5).
    s_link.onTelemetryPublish([](JsonObject doc) {
        if (g_vocIndex >= 0) {
            doc["units"][0]["vocIndex"] = g_vocIndex;
        }
    });

    // Fiche: capteur + organes de commande (chapitre 6).
    s_link.card().sensor("voc", "VOC index", "", "units[0].vocIndex");

    static const char* kModes[] = { "auto", "on", "off" };
    s_link.card().select("mode", "Mode", kModes, 3, [](const char* opt) {
        onModeSelected(opt);
    });

    s_link.card().number("threshold", "VOC threshold", 100, 400, 10, "", [](float v) {
        onThresholdChanged(v);
    });

    // Mise en page (chapitre 6, optionnel).
    s_link.card().layoutRow("voc", "fan");
    s_link.card().layoutRow("mode", "threshold");
}

6. Vérification des scénarios

Action Attente
Mode auto, souffler sur le capteur VOC croît, au seuil le ventilateur démarre, fiche affiche « Allumé »
Air nettoyé en-dessous du seuil−20 le ventilateur s'éteint seul
Mode on depuis le portail ventilateur tourne indépendamment de VOC
Mode off depuis le portail ventilateur arrêté, VOC continue d'afficher
Redémarrage de la carte mode et seuil sauvegardés

7. Code final : src/main.cpp complet

Tout le code des chapitres 4–7, rassemblé en un seul fichier. Si quelque chose ne correspond pas au vôtre — vérifiez avec ce listing.

// ============================================================
// Filtre d'air intelligent sur idryer-core.
// SGP40 (VOC) + ventilateur via MOSFET, mode auto/manuel,
// commande et fiche sur portail via card-manifeste.
// ============================================================

#include <iDryer.h>
#include <Wire.h>
#include <Adafruit_SGP40.h>
#include <Preferences.h>

// ── Broches ──────────────────────────────────────────────────
static const int FAN_PIN = 4;         // grille MOSFET du ventilateur
// SDA=8, SCL=9 — définies dans Wire.begin() ci-dessous

// ── Passeport de l'appareil (chapitre 4) ─────────────────────
static const iDryer::Config CFG = {
    .deviceType        = iDryer::DeviceType::Unknown, // appareil non-standard
    .unitsCount        = 1,
    .hasFan            = true,        // seule compétence du dictionnaire
    .telemetryPeriodMs = 5000,
    .statusPeriodMs    = 10000,
    .hardwareVersion   = "1.0",
    .firmwareVersion   = "0.1.0",
    .model             = "DIY Air Filter",
};

static iDryer::Link s_link(CFG);

// ── État (chapitre 7) ────────────────────────────────────────
enum class FilterMode : uint8_t { Auto, On, Off };
static FilterMode g_mode      = FilterMode::Auto;
static int32_t    g_threshold = 150;  // indice VOC de déclenchement
static bool       g_fanOn     = false;

static Preferences s_prefs;           // NVS: paramètres survivent au redémarrage

// ── Capteur VOC (chapitre 5) ────────────────────────────────
static Adafruit_SGP40 s_sgp;
static int32_t g_vocIndex = -1;       // -1 = pas encore de données

static void initVocSensor() {
    Wire.begin(/*SDA=*/8, /*SCL=*/9);
    if (!s_sgp.begin()) {
        Serial.println("[VOC] SGP40 not found, check wiring");
    }
}

static void readVocSensor() {
    // Indice: ~100 = air normal, plus haut = plus sale (max 500).
    g_vocIndex = s_sgp.measureVocIndex();
}

// ── Ventilateur (chapitre 7) ────────────────────────────────
static void setFan(bool on) {         // on: true = allumer, false = éteindre
    if (g_fanOn == on) return;        // déjà dans l'état désiré
    g_fanOn = on;
    digitalWrite(FAN_PIN, on ? HIGH : LOW);
    s_link.telemetry.fanOn[0] = on;   // champ du dictionnaire → cloud → fiche
    s_link.publishTelemetryNow();     // changement d'état — publier tout de suite
}

// ── Automatisation avec hystérésis (chapitre 7) ──────────────
static void tickAutoLogic() {
    if (g_mode == FilterMode::On)  { setFan(true);  return; }
    if (g_mode == FilterMode::Off) { setFan(false); return; }

    // Auto: allumage au seuil, éteignage 20 points au-dessous.
    if (g_vocIndex < 0) return;       // capteur est muet encore
    if (!g_fanOn && g_vocIndex >= g_threshold)      setFan(true);
    if ( g_fanOn && g_vocIndex <= g_threshold - 20) setFan(false);
}

// ── Callbacks de commandes depuis le portail (chapitres 6–7) ──
static void onModeSelected(const char* opt) {
    if      (strcmp(opt, "auto") == 0) g_mode = FilterMode::Auto;
    else if (strcmp(opt, "on")   == 0) g_mode = FilterMode::On;
    else                               g_mode = FilterMode::Off;
    s_prefs.putUChar("mode", (uint8_t)g_mode);
    tickAutoLogic();                  // appliquer tout de suite
}

static void onThresholdChanged(float v) {
    g_threshold = (int32_t)v;
    s_prefs.putInt("thr", g_threshold);
}

// ── setup: paramètres, réseau, capteur, fiche ───────────────
void setup() {
    Serial.begin(115200);

    // Paramètres depuis NVS (deuxièmes arguments — défauts du premier démarrage).
    s_prefs.begin("filter");
    g_mode      = (FilterMode)s_prefs.getUChar("mode", (uint8_t)FilterMode::Auto);
    g_threshold = s_prefs.getInt("thr", 150);

    pinMode(FAN_PIN, OUTPUT);

    s_link.begin();                   // Wi-Fi, MQTT, attachement — tout dedans
    initVocSensor();

    // Télémétrie: ajout du champ vocIndex personnel (chapitre 5).
    s_link.onTelemetryPublish([](JsonObject doc) {
        if (g_vocIndex >= 0) {
            doc["units"][0]["vocIndex"] = g_vocIndex;
        }
    });

    // Fiche: capteur + organes de commande (chapitre 6).
    s_link.card().sensor("voc", "VOC index", "", "units[0].vocIndex");

    static const char* kModes[] = { "auto", "on", "off" };
    s_link.card().select("mode", "Mode", kModes, 3, [](const char* opt) {
        onModeSelected(opt);
    });

    s_link.card().number("threshold", "VOC threshold", 100, 400, 10, "", [](float v) {
        onThresholdChanged(v);
    });

    // Mise en page usine de la fiche (chapitre 6, optionnel).
    s_link.card().layoutRow("voc", "fan");
    s_link.card().layoutRow("mode", "threshold");
}

// ── loop: réseau toujours, capteur et logique une fois par seconde ──
static uint32_t s_lastReadMs = 0;

void loop() {
    s_link.loop();                    // réseau, télémétrie, commandes — toujours d'abord

    uint32_t now = millis();
    if (now - s_lastReadMs >= 1000) {
        s_lastReadMs = now;
        readVocSensor();              // lecture fraîche…
        tickAutoLogic();              // …et aussitôt décision selon elle
    }
}