Logika automatiky¶
Spojujeme všechno: senzor rozhoduje, ventilátor točí, portál řídí.
1. Stav a nastavení¶
Na začátek 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; // VOC index zapnutí
static bool g_fanOn = false;
static Preferences s_prefs;
Preferences (NVS) — energeticky nezávislá paměť ESP32: režim a práh přežijí restart.
2. Řízení ventilátoru¶
Veškeré zapínání a vypínání svedeme do jediné funkce setFan. Přijímá jeden argument on — požadovaný stav: true = zapnout, false = vypnout. Dál v kódu vždy voláme setFan(true) / setFan(false) a ona obstará veškerou rutinu: přepne pin, zapamatuje si stav a oznámí to portálu.
static void setFan(bool on) { // on — argument: true = zapnout, false = vypnout
if (g_fanOn == on) return; // již v požadovaném stavu — nic neděláme
g_fanOn = on; // zapamatujeme si nový stav v globální proměnné
digitalWrite(FAN_PIN, on ? HIGH : LOW); // fyzicky zapínáme/vypínáme klíč ventilátoru
// Řekneme stav jádru: fanOn[0] — slovníkové pole telemetrických dat
// (objevilo se kvůli hasFan = true; [0] — náš jediný blok, jak v kapitole 5).
// Odtud si to vezme cloud a na buňku „Ventilátor" karty.
s_link.telemetry.fanOn[0] = on;
// Změna stavu — důvod poslat telemetrické údaje hned, nečekat na období.
s_link.publishTelemetryNow();
}
publishTelemetryNow() zajistí okamžitou odezvu: kliknuli v portálu — za sekundu karta zobrazuje potvrzený stav. Právě potvrzený: portál iDryer stav nikdy „neodhaduje", zobrazuje výhradně to, co zařízení skutečně odeslalo.
3. Automatika s hysterezí¶
Kdybychom spínali přesně na prahu, ventilátor by kolem prahové hodnoty kmital zap/vyp. Léčí se mrtvým pásmem:
static void tickAutoLogic() {
if (g_mode == FilterMode::On) { setFan(true); return; }
if (g_mode == FilterMode::Off) { setFan(false); return; }
// Auto: zapínáme na prahu, vypínáme o 20 bodů níže.
if (g_vocIndex < 0) return; // senzor ještě mlčí
if (!g_fanOn && g_vocIndex >= g_threshold) setFan(true);
if ( g_fanOn && g_vocIndex <= g_threshold - 20) setFan(false);
}
Funkci tickAutoLogic() budeme volat na stejném místě jako čtení senzoru — v loop() podle sekundního časovače. Jde o ten samý loop() z kapitoly 5, přidáváme jediný řádek. Celý vypadá takto:
void loop() {
s_link.loop(); // síť, telemetrické údaje, příkazy — vždy první
uint32_t now = millis();
if (now - s_lastReadMs >= 1000) { // jednou za sekundu:
s_lastReadMs = now;
readVocSensor(); // čteme VOC (kapitola 5)
tickAutoLogic(); // a hned se rozhodujeme o ventilátoru
}
}
Pořadí uvnitř sekundního bloku není náhodné: nejdřív čerstvé čtení senzoru, potom rozhodnutí na jeho základě.
4. Callbacky z portálu¶
To jsou ty samé funkce, které jsme slibovali v kapitole 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(); // aplikujeme hned, nečekáme na příští tik
}
static void onThresholdChanged(float v) {
g_threshold = (int32_t)v;
s_prefs.putInt("thr", g_threshold);
}
Tyto funkce nahrazují zástupce z kapitoly 6 — prázdné verze smažte.
Všimněte si, co v tomto kódu chybí: parsování MQTT, topiky, JSON příkazy. Uživatel vybral on v seznamu v portálu → jádro příkaz přijalo, ověřilo a zavolalo onModeSelected("on"). Veškerá transportní mechanika je starostí jádra.
5. Finální setup()¶
Do setup() zbývá přidat dvě věci: načtení uložených nastavení z NVS (na začátek, aby logika s nimi pracovala hned od startu) a nastavení pinu ventilátoru. Celý setup() po této kapitole vypadá takto:
void setup() {
Serial.begin(115200);
// Nastavení z NVS: to, co si uživatel nastavil naposledy.
s_prefs.begin("filter"); // otevřít jmenný prostor "filter" v NVS
g_mode = (FilterMode)s_prefs.getUChar("mode", (uint8_t)FilterMode::Auto);
g_threshold = s_prefs.getInt("thr", 150);
// Druhé argumenty getUChar/getInt — výchozí hodnoty: vrátí se
// při prvním spuštění, kdy v NVS ještě nic není uloženo.
pinMode(FAN_PIN, OUTPUT); // pin klíče ventilátoru — na výstup
s_link.begin();
initVocSensor();
// Telemetrické údaje: vlastní pole vocIndex (kapitola 5).
s_link.onTelemetryPublish([](JsonObject doc) {
if (g_vocIndex >= 0) {
doc["units"][0]["vocIndex"] = g_vocIndex;
}
});
// Karta: senzor + ovládací prvky (kapitola 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);
});
// Rozvržení (kapitola 6, podle přání).
s_link.card().layoutRow("voc", "fan");
s_link.card().layoutRow("mode", "threshold");
}
6. Ověření scénářů¶
| Akce | Očekávaný výsledek |
|---|---|
Režim auto, vydechnout na senzor |
VOC roste, na prahu se ventilátor zapne, karta zobrazí „Zap" |
| Vzduch se vyčistil | pod prahem−20 se ventilátor sám vypne |
Režim on z portálu |
ventilátor běží nezávisle na VOC |
Režim off z portálu |
ventilátor stojí, VOC se nadále zobrazuje |
| Restart desky | režim a práh se zachovaly |
7. Výsledný kód: src/main.cpp celý¶
Veškerý kód kapitol 4–7, složený do jednoho souboru. Pokud se něco neshoduje s vaší verzí — srovnávejte s tímto výpisem.
// ============================================================
// Chytrý vzduchový filtr na idryer-core.
// SGP40 (VOC) + ventilátor skrz MOSFET, auto/ruční režim,
// řízení a karta v portálu skrz card manifest.
// ============================================================
#include <iDryer.h>
#include <Wire.h>
#include <Adafruit_SGP40.h>
#include <Preferences.h>
// ── Piny ────────────────────────────────────────────────────
static const int FAN_PIN = 4; // gate MOSFET ventilátoru
// SDA=8, SCL=9 — nastavují se v Wire.begin() níže
// ── Konfigurace zařízení (kapitola 4) ───────────────────
static const iDryer::Config CFG = {
.deviceType = iDryer::DeviceType::Unknown, // nestandardní zařízení
.unitsCount = 1,
.hasFan = true, // jediná slovníková schopnost
.telemetryPeriodMs = 5000,
.statusPeriodMs = 10000,
.hardwareVersion = "1.0",
.firmwareVersion = "0.1.0",
.model = "DIY Air Filter",
};
static iDryer::Link s_link(CFG);
// ── Stav (kapitola 7) ─────────────────────────────────────
enum class FilterMode : uint8_t { Auto, On, Off };
static FilterMode g_mode = FilterMode::Auto;
static int32_t g_threshold = 150; // VOC index zapnutí
static bool g_fanOn = false;
static Preferences s_prefs; // NVS: nastavení přežijí restart
// ── Senzor VOC (kapitola 5) ────────────────────────────────
static Adafruit_SGP40 s_sgp;
static int32_t g_vocIndex = -1; // -1 = údaje ještě nejsou
static void initVocSensor() {
Wire.begin(/*SDA=*/8, /*SCL=*/9);
if (!s_sgp.begin()) {
Serial.println("[VOC] SGP40 not found, check wiring");
}
}
static void readVocSensor() {
// Index: ~100 = normální vzduch, více = špinavěji (max 500).
g_vocIndex = s_sgp.measureVocIndex();
}
// ── Ventilátor (kapitola 7) ────────────────────────────────
static void setFan(bool on) { // on: true = zapnout, false = vypnout
if (g_fanOn == on) return; // již v požadovaném stavu
g_fanOn = on;
digitalWrite(FAN_PIN, on ? HIGH : LOW);
s_link.telemetry.fanOn[0] = on; // slovníkové pole → cloud → karta
s_link.publishTelemetryNow(); // změna stavu — publikujeme hned
}
// ── Automatika s hysterezí (kapitola 7) ─────────────────────
static void tickAutoLogic() {
if (g_mode == FilterMode::On) { setFan(true); return; }
if (g_mode == FilterMode::Off) { setFan(false); return; }
// Auto: zapínáme na prahu, vypínáme o 20 bodů níže.
if (g_vocIndex < 0) return; // senzor ještě mlčí
if (!g_fanOn && g_vocIndex >= g_threshold) setFan(true);
if ( g_fanOn && g_vocIndex <= g_threshold - 20) setFan(false);
}
// ── Callbacky příkazů z portálu (kapitoly 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(); // aplikujeme hned
}
static void onThresholdChanged(float v) {
g_threshold = (int32_t)v;
s_prefs.putInt("thr", g_threshold);
}
// ── setup: nastavení, síť, senzor, karta ────────────────
void setup() {
Serial.begin(115200);
// Nastavení z NVS (druhé argumenty — výchozí hodnoty prvního spuštění).
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, připojení — vše uvnitř
initVocSensor();
// Telemetrické údaje: doplňujeme vlastní pole vocIndex (kapitola 5).
s_link.onTelemetryPublish([](JsonObject doc) {
if (g_vocIndex >= 0) {
doc["units"][0]["vocIndex"] = g_vocIndex;
}
});
// Karta: senzor + ovládací prvky (kapitola 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);
});
// Tovární rozvržení karty (kapitola 6, podle přání).
s_link.card().layoutRow("voc", "fan");
s_link.card().layoutRow("mode", "threshold");
}
// ── loop: síť vždy, senzor a logika jednou za sekundu ────
static uint32_t s_lastReadMs = 0;
void loop() {
s_link.loop(); // síť, telemetrické údaje, příkazy — vždy první
uint32_t now = millis();
if (now - s_lastReadMs >= 1000) {
s_lastReadMs = now;
readVocSensor(); // čerstvé čtení…
tickAutoLogic(); // …a hned rozhodnutí podle něj
}
}