Lógica de automatização¶
Conectamos tudo: 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 de activação
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ções.
2. Controlo do ventilador¶
Reunimos toda ligação e desligação numa função setFan. Ela toma um argumento on — estado desejado: true = ligar, false = desligar. Adiante no código sempre chamamos setFan(true) / setFan(false), e ela faz toda a rotina: muda o pino, memoriza o estado e informa 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; // memorizamos o novo estado na variável global
digitalWrite(FAN_PIN, on ? HIGH : LOW); // fisicamente ligamos/desligamos a chave do ventilador
// Informamos o estado ao núcleo: fanOn[0] — campo de telemetria de dicionário
// (apareceu porque hasFan = true; [0] — nosso único unit, como no capítulo 5).
// Daqui vai para a nuvem e para a 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 pelo período.
s_link.publishTelemetryNow();
}
publishTelemetryNow() torna a resposta instantânea: clicou no portal — em um segundo o cartão mostra o estado confirmado. Precisamente confirmado: o portal iDryer nunca «adivinha» o estado, mostra o que o dispositivo realmente enviou.
3. Automatização com histerese¶
Se ligarmos o ventilador exactamente no limiar, à volta do limiar ele vai oscilar entre lig/deslig. Corrige-se com um intervalo:
static void tickAutoLogic() {
if (g_mode == FilterMode::On) { setFan(true); return; }
if (g_mode == FilterMode::Off) { setFan(false); return; }
// Auto: ligamo-nos no limiar, desligamo-nos 20 pontos abaixo.
if (g_vocIndex < 0) return; // sensor ainda está silencioso
if (!g_fanOn && g_vocIndex >= g_threshold) setFan(true);
if ( g_fanOn && g_vocIndex <= g_threshold - 20) setFan(false);
}
Chamamos tickAutoLogic() onde lemos o sensor — em loop() com temporizador de segundo. Este é o loop() do capítulo 5, acrescenta-se uma linha. Completo fica agora 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 já decidimos sobre o ventilador
}
}
A ordem dentro do bloco de segundo não é acidental: primeira leitura fresca do sensor, depois decisão sobre ela.
4. Callbacks do portal¶
Aquelas 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 esperamos pelo próximo tick
}
static void onThresholdChanged(float v) {
g_threshold = (int32_t)v;
s_prefs.putInt("thr", g_threshold);
}
Estas funções substituem os stubs do capítulo 6 — delete as versões vazias.
Note o que não há neste código: desempacotamento de MQTT, tópicos, comandos JSON. O utilizador 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¶
Falta adicionar em setup() duas coisas: carregar as configurações guardadas em NVS (no início, para a lógica trabalhar logo com elas) e configurar o pino do ventilador. O setup() completo após este capítulo fica assim:
void setup() {
Serial.begin(115200);
// Configurações de NVS: o que o utilizador escolheu outras vezes.
s_prefs.begin("filter"); // abrir espaço de nomes "filter" em NVS
g_mode = (FilterMode)s_prefs.getUChar("mode", (uint8_t)FilterMode::Auto);
g_threshold = s_prefs.getInt("thr", 150);
// Segundos argumentos de getUChar/getInt — valores por defeito: retornam
// na primeira execução, quando NVS ainda está vazio.
pinMode(FAN_PIN, OUTPUT); // pino da chave do ventilador — para saída
s_link.begin();
initVocSensor();
// Telemetria: campo próprio 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 controlo (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 | Expectativa |
|---|---|
Modo auto, respirar sobre o sensor |
VOC cresce, no limiar ventilador liga, cartão mostra «Lig» |
| Ar se limpa | abaixo de limiar−20 ventilador desliga sozinho |
Modo on a partir do portal |
ventilador gira independentemente de VOC |
Modo off a partir do portal |
ventilador parado, VOC continua mostrando-se |
| Reinicializar a placa | modo e limiar guardaram-se |
7. Código final: src/main.cpp inteiro¶
Todo o código dos capítulos 4–7, reunido num ficheiro. Se algo não corresponder ao seu — compare com esta listagem.
// ============================================================
// Filtro de ar inteligente em idryer-core.
// SGP40 (VOC) + ventilador através de MOSFET, modo auto/manual,
// controlo e cartão no portal através do manifesto de cartão.
// ============================================================
#include <iDryer.h>
#include <Wire.h>
#include <Adafruit_SGP40.h>
#include <Preferences.h>
// ── Pinos ────────────────────────────────────────────────────
static const int FAN_PIN = 4; // gate MOSFET do ventilador
// SDA=8, SCL=9 — definem-se em Wire.begin() abaixo
// ── Passaporte do dispositivo (capítulo 4) ──────────────────
static const iDryer::Config CFG = {
.deviceType = iDryer::DeviceType::Unknown, // dispositivo não-standard
.unitsCount = 1,
.hasFan = true, // única capacidade de 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 de activação
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, superior = 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 de dicionário → nuvem → cartão
s_link.publishTelemetryNow(); // mudança de estado — publicamos já
}
// ── Automatização 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: ligamo-nos no limiar, desligamo-nos 20 pontos abaixo.
if (g_vocIndex < 0) return; // sensor ainda está 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 — defeitos primeira execução).
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, ligação — tudo dentro
initVocSensor();
// Telemetria: adicionamos o campo próprio 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 controlo (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 do cartão de fábrica (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 já decisão sobre ela
}
}