Pular para conteúdo

Lógica de automação

Conectamos tudo: o sensor decide, ventilador gira, portal controla.

1. Estado e configurações

No início 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;   // índice VOC para ativar
static bool       g_fanOn     = false;

static Preferences s_prefs;

Preferences (NVS) — memória não-volátil do ESP32: modo e limiar sobrevivem a reinicialização.

2. Controle do ventilador

Concentramos todo ligar e desligar em uma função setFan. Ela recebe um argumento on — estado desejado: true = ligar, false = desligar. Mais abaixo no código sempre chamamos setFan(true) / setFan(false), e ela faz toda a rotina: move o pino, lembra o estado e avisa o portal.

static void setFan(bool on) {      // on — argumento: true = ligar, false = desligar
    if (g_fanOn == on) return;     // já no estado desejado — nada fazer
    g_fanOn = on;                  // lembramos novo estado em variável global
    digitalWrite(FAN_PIN, on ? HIGH : LOW);  // ligamos/desligamos chave do ventilador fisicamente

    // Comunicamos ao núcleo: fanOn[0] — campo de telemetria do dicionário
    // (apareceu de hasFan = true; [0] — nossa única unidade, como no capítulo 5).
    // Daqui vai para nuvem e célula "Ventilador" do cartão.
    s_link.telemetry.fanOn[0] = on;

    // Mudança de estado — razão para enviar telemetria já, não esperar o período.
    s_link.publishTelemetryNow();
}

publishTelemetryNow() faz a resposta instantânea: clicou no portal — em um segundo o cartão mostra o estado confirmado. Confirmado de verdade: o portal iDryer nunca "adivinha" o estado, mostra o que o dispositivo realmente mandou.

3. Automática com histerese

Se ligarmos o ventilador exatamente no limiar, perto do limiar ele vai oscilar entre lig/deslig. Resolve-se com uma margem:

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

    // Auto: ligamos no limiar, desligamos 20 pontos abaixo.
    if (g_vocIndex < 0) return;                      // sensor ainda silencioso
    if (!g_fanOn && g_vocIndex >= g_threshold)      setFan(true);
    if ( g_fanOn && g_vocIndex <= g_threshold - 20) setFan(false);
}

Chamaremos tickAutoLogic() ali mesmo onde lemos o sensor — em loop() com timer de segundo. Este é o loop() do capítulo 5, nele adicionamos uma linha. Completamente agora fica assim:

void loop() {
    s_link.loop();                        // rede, telemetria, comandos — sempre primeiro

    uint32_t now = millis();
    if (now - s_lastReadMs >= 1000) {     // uma vez por segundo:
        s_lastReadMs = now;
        readVocSensor();                  //   lemos VOC (capítulo 5)
        tickAutoLogic();                  //   e logo decidimos sobre ventilador
    }
}

Ordem dentro do bloco de segundo não é acidental: primeiro leitura fresca do sensor, depois decisão por ela.

4. Callbacks do portal

Aquelas mesmas funções que prometemos no capítulo 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();   // aplicamos já, não esperar próximo tick
}

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

Estas funções substituem os esboços do capítulo 6 — delete as versões vazias.

Note o que não está neste código: análise de MQTT, tópicos, comandos JSON. O usuário escolheu on na lista no portal → o núcleo recebeu o comando, verificou e chamou onModeSelected("on"). Toda a mecânica de transporte é responsabilidade do núcleo.

5. setup() final

Resta adicionar em setup() duas coisas: carregamento das configurações salvas em NVS (no início, para a lógica já trabalhar com elas) e configuração do pino do ventilador. O setup() completo após este capítulo fica assim:

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

    // Configurações de NVS: o que usuário escolhia em tempos anteriores.
    s_prefs.begin("filter");   // abrir namespace "filter" em NVS
    g_mode      = (FilterMode)s_prefs.getUChar("mode", (uint8_t)FilterMode::Auto);
    g_threshold = s_prefs.getInt("thr", 150);
    // Segundos argumentos getUChar/getInt — valores padrão: retornarão
    // no primeiro lançamento, quando ainda nada está salvo em NVS.

    pinMode(FAN_PIN, OUTPUT);  // pino chave do ventilador — para saída

    s_link.begin();
    initVocSensor();

    // Telemetria: seu campo vocIndex (capítulo 5).
    s_link.onTelemetryPublish([](JsonObject doc) {
        if (g_vocIndex >= 0) {
            doc["units"][0]["vocIndex"] = g_vocIndex;
        }
    });

    // Cartão: sensor + órgãos de controle (capítulo 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);
    });

    // Layout (capítulo 6, opcional).
    s_link.card().layoutRow("voc", "fan");
    s_link.card().layoutRow("mode", "threshold");
}

6. Verificação de cenários

Ação Esperado
Modo auto, soprar no sensor VOC cresce, no limiar ventilador liga, cartão mostra "Lig"
Ar limpou abaixo de limiar−20 ventilador desliga sozinho
Modo on do portal ventilador gira independente de VOC
Modo off do portal ventilador parado, VOC continua mostrando
Reiniciar placa modo e limiar foram salvos

7. Código final: src/main.cpp completamente

Todo o código dos capítulos 4–7, montado em um arquivo. Se algo não coincidir com o seu — compare com esta listagem.

// ============================================================
// Filtro de ar inteligente em idryer-core.
// SGP40 (VOC) + ventilador pelo MOSFET, modo auto/manual,
// controle e cartão no portal através de manifesto do cartão.
// ============================================================

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

// ── Pinos ────────────────────────────────────────────────────
static const int FAN_PIN = 4;         // porta MOSFET do ventilador
// SDA=8, SCL=9 — especificados em Wire.begin() abaixo

// ── Passaporte do dispositivo (capítulo 4) ───────────────────
static const iDryer::Config CFG = {
    .deviceType        = iDryer::DeviceType::Unknown, // dispositivo não-padrão
    .unitsCount        = 1,
    .hasFan            = true,        // única habilidade do dicionário
    .telemetryPeriodMs = 5000,
    .statusPeriodMs    = 10000,
    .hardwareVersion   = "1.0",
    .firmwareVersion   = "0.1.0",
    .model             = "DIY Air Filter",
};

static iDryer::Link s_link(CFG);

// ── Estado (capítulo 7) ─────────────────────────────────────
enum class FilterMode : uint8_t { Auto, On, Off };
static FilterMode g_mode      = FilterMode::Auto;
static int32_t    g_threshold = 150;  // índice VOC para ativar
static bool       g_fanOn     = false;

static Preferences s_prefs;           // NVS: configurações sobrevivem reinicialização

// ── Sensor VOC (capítulo 5) ────────────────────────────────────
static Adafruit_SGP40 s_sgp;
static int32_t g_vocIndex = -1;       // -1 = sem dados ainda

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

static void readVocSensor() {
    // Índice: ~100 = ar normal, maior = mais sujo (máx 500).
    g_vocIndex = s_sgp.measureVocIndex();
}

// ── Ventilador (capítulo 7) ────────────────────────────────────
static void setFan(bool on) {         // on: true = ligar, false = desligar
    if (g_fanOn == on) return;        // já no estado desejado
    g_fanOn = on;
    digitalWrite(FAN_PIN, on ? HIGH : LOW);
    s_link.telemetry.fanOn[0] = on;   // campo do dicionário → nuvem → cartão
    s_link.publishTelemetryNow();     // mudança de estado — publicamos já
}

// ── Automática com histerese (capítulo 7) ─────────────────────
static void tickAutoLogic() {
    if (g_mode == FilterMode::On)  { setFan(true);  return; }
    if (g_mode == FilterMode::Off) { setFan(false); return; }

    // Auto: ligamos no limiar, desligamos 20 pontos abaixo.
    if (g_vocIndex < 0) return;       // sensor ainda silencioso
    if (!g_fanOn && g_vocIndex >= g_threshold)      setFan(true);
    if ( g_fanOn && g_vocIndex <= g_threshold - 20) setFan(false);
}

// ── Callbacks de comandos do portal (capítulos 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();                  // aplicamos já
}

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

// ── setup: configurações, rede, sensor, cartão ────────────────
void setup() {
    Serial.begin(115200);

    // Configurações de NVS (segundos argumentos — padrões do primeiro lançamento).
    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, vinculação — tudo dentro
    initVocSensor();

    // Telemetria: adicionamos nosso campo vocIndex (capítulo 5).
    s_link.onTelemetryPublish([](JsonObject doc) {
        if (g_vocIndex >= 0) {
            doc["units"][0]["vocIndex"] = g_vocIndex;
        }
    });

    // Cartão: sensor + órgãos de controle (capítulo 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);
    });

    // Layout de fábrica do cartão (capítulo 6, opcional).
    s_link.card().layoutRow("voc", "fan");
    s_link.card().layoutRow("mode", "threshold");
}

// ── loop: rede sempre, sensor e lógica uma vez por segundo ────────
static uint32_t s_lastReadMs = 0;

void loop() {
    s_link.loop();                    // rede, telemetria, comandos — sempre primeiro

    uint32_t now = millis();
    if (now - s_lastReadMs >= 1000) {
        s_lastReadMs = now;
        readVocSensor();              // leitura fresca…
        tickAutoLogic();              // …e logo decisão por ela
    }
}