Automatik-Logik¶
Verbinden wir alles: Der Sensor entscheidet, der Lüfter dreht, das Portal steuert.
1. Status und Einstellungen¶
Am Anfang von 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 zum Einschalten
static bool g_fanOn = false;
static Preferences s_prefs;
Preferences (NVS) – nicht-flüchtiger Speicher des ESP32: Modus und Schwelle überleben Neustart.
2. Lüfter-Steuerung¶
Alles Ein- und Ausschalten fassen wir in eine Funktion setFan. Sie nimmt ein Argument on – gewünschter Status: true = einschalten, false = ausschalten. Im Rest des Codes rufen wir immer setFan(true) / setFan(false) auf, sie macht ganze Routine: dreht Pin, merkt sich Status und meldet Portal.
static void setFan(bool on) { // on – Argument: true = einschalten, false = ausschalten
if (g_fanOn == on) return; // schon in gewünschtem Status – nichts tun
g_fanOn = on; // neuen Status in globaler Variablen merken
digitalWrite(FAN_PIN, on ? HIGH : LOW); // physisch Lüfter-Schalter ein-/ausschalten
// Status dem Kern melden: fanOn[0] – Wörterbuch-Telemetrie-Feld
// (erschien aus hasFan = true; [0] – unsere einzige Unit, wie in Kapitel 5).
// Daher geht es in die Cloud und auf die „Lüfter"-Zelle der Karte.
s_link.telemetry.fanOn[0] = on;
// Statusänderung – Grund, Telemetrie sofort zu senden, nicht auf Periode zu warten.
s_link.publishTelemetryNow();
}
publishTelemetryNow() macht Antwort sofortig: Knopf im Portal gedrückt – innerhalb einer Sekunde zeigt Karte bestätigten Status. Genau bestätigten: iDryer-Portal „rät" nie Status, es zeigt, was Gerät wirklich gesendet hat.
3. Automatik mit Hysterese¶
Wenn Lüfter genau auf Schwelle schaltet, daran flattert er ein/aus. Abhilfe – Lücke:
static void tickAutoLogic() {
if (g_mode == FilterMode::On) { setFan(true); return; }
if (g_mode == FilterMode::Off) { setFan(false); return; }
// Auto: schalten an Schwelle ein, 20 Punkte darunter aus.
if (g_vocIndex < 0) return; // Sensor noch stumm
if (!g_fanOn && g_vocIndex >= g_threshold) setFan(true);
if ( g_fanOn && g_vocIndex <= g_threshold - 20) setFan(false);
}
Aufrufen tickAutoLogic() dort, wo wir Sensor lesen – in loop() per Sekunden-Timer. Das ist eben der loop() aus Kapitel 5, dazu eine Zeile. Ganz sieht es jetzt so aus:
void loop() {
s_link.loop(); // Netz, Telemetrie, Befehle – immer zuerst
uint32_t now = millis();
if (now - s_lastReadMs >= 1000) { // pro Sekunde:
s_lastReadMs = now;
readVocSensor(); // VOC lesen (Kapitel 5)
tickAutoLogic(); // und sofort über Lüfter entscheiden
}
}
Reihenfolge innerhalb Sekunden-Block nicht zufällig: erst frischer Sensor-Messwert, dann Entscheidung.
4. Callbacks vom Portal¶
Jene Funktionen, die wir in Kapitel 6 versprachen:
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(); // sofort anwenden, nicht auf nächsten Tick warten
}
static void onThresholdChanged(float v) {
g_threshold = (int32_t)v;
s_prefs.putInt("thr", g_threshold);
}
Diese Funktionen ersetzen Stubs aus Kapitel 6 – leere Versionen löschen.
Achten Sie, was in diesem Code nicht ist: MQTT-Parsing, Topics, JSON-Befehle. Nutzer wählte on in der Portal-Liste → Kern fing Befehl auf, überprüfte und rief onModeSelected("on") auf. Ganze Transport-Mechanik – Kernverantwortung.
5. Endgültiges setup()¶
Noch zwei Dinge hinzufügen zu setup(): Laden gespeicherter Einstellungen aus NVS (am Anfang, damit Logik gleich damit arbeitet) und Einstellen des Lüfter-Pins. Ganz setup() nach diesem Kapitel so:
void setup() {
Serial.begin(115200);
// Einstellungen aus NVS: was Nutzer in früheren Male wählte.
s_prefs.begin("filter"); // „filter"-Namespace in NVS öffnen
g_mode = (FilterMode)s_prefs.getUChar("mode", (uint8_t)FilterMode::Auto);
g_threshold = s_prefs.getInt("thr", 150);
// Zweite Argumente getUChar/getInt – Standard-Werte: rückgeben bei
// erstem Start, wenn in NVS noch nichts gespeichert.
pinMode(FAN_PIN, OUTPUT); // Lüfter-Schlüssel-Pin – auf Ausgang
s_link.begin();
initVocSensor();
// Telemetrie: eigenes Feld vocIndex (Kapitel 5).
s_link.onTelemetryPublish([](JsonObject doc) {
if (g_vocIndex >= 0) {
doc["units"][0]["vocIndex"] = g_vocIndex;
}
});
// Karte: Sensor + Steuerelemente (Kapitel 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 (Kapitel 6, optional).
s_link.card().layoutRow("voc", "fan");
s_link.card().layoutRow("mode", "threshold");
}
6. Szenarien-Überprüfung¶
| Aktion | Erwartet |
|---|---|
Modus auto, auf Sensor pusten |
VOC wächst, auf Schwelle Lüfter anspringt, Karte zeigt „An" |
| Luft gereinigt | Unter Schwelle−20 Lüfter schaltet selbst aus |
Modus on vom Portal |
Lüfter dreht unabhängig von VOC |
Modus off vom Portal |
Lüfter still, VOC zeigt weiter |
| Platte neu gestartet | Modus und Schwelle gespeichert |
7. Kompletter Code: src/main.cpp ganz¶
Ganzer Code Kapitel 4–7, in einer Datei gesammelt. Falls etwas mit Ihrem nicht stimmt – vergleichen Sie mit diesem Listing.
// ============================================================
// Intelligenter Luftfilter auf idryer-core.
// SGP40 (VOC) + Lüfter per MOSFET, auto/Handbetrieb,
// Steuerung und Karte im Portal durch Card-Manifest.
// ============================================================
#include <iDryer.h>
#include <Wire.h>
#include <Adafruit_SGP40.h>
#include <Preferences.h>
// ── Pins ────────────────────────────────────────────────────
static const int FAN_PIN = 4; // Lüfter-MOSFET Gate
// SDA=8, SCL=9 – vorgeben in Wire.begin() unten
// ── Geräte-Passpord (Kapitel 4) ────────────────────────────
static const iDryer::Config CFG = {
.deviceType = iDryer::DeviceType::Unknown, // unbekanntes Gerät
.unitsCount = 1,
.hasFan = true, // einzige Wörterbuch-Fähigkeit
.telemetryPeriodMs = 5000,
.statusPeriodMs = 10000,
.hardwareVersion = "1.0",
.firmwareVersion = "0.1.0",
.model = "DIY Air Filter",
};
static iDryer::Link s_link(CFG);
// ── Status (Kapitel 7) ─────────────────────────────────────
enum class FilterMode : uint8_t { Auto, On, Off };
static FilterMode g_mode = FilterMode::Auto;
static int32_t g_threshold = 150; // VOC-Index zum Einschalten
static bool g_fanOn = false;
static Preferences s_prefs; // NVS: Einstellungen überleben Neustart
// ── VOC-Sensor (Kapitel 5) ────────────────────────────────────
static Adafruit_SGP40 s_sgp;
static int32_t g_vocIndex = -1; // -1 = noch keine Daten
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 = normale Luft, höher = schmutziger (max 500).
g_vocIndex = s_sgp.measureVocIndex();
}
// ── Lüfter (Kapitel 7) ────────────────────────────────────
static void setFan(bool on) { // on: true = einschalten, false = ausschalten
if (g_fanOn == on) return; // schon in gewünschtem Status
g_fanOn = on;
digitalWrite(FAN_PIN, on ? HIGH : LOW);
s_link.telemetry.fanOn[0] = on; // Wörterbuch-Feld → Cloud → Karte
s_link.publishTelemetryNow(); // Status-Änderung – sofort veröffentlichen
}
// ── Automatik mit Hysterese (Kapitel 7) ─────────────────────
static void tickAutoLogic() {
if (g_mode == FilterMode::On) { setFan(true); return; }
if (g_mode == FilterMode::Off) { setFan(false); return; }
// Auto: an Schwelle einschalten, 20 Punkte darunter ausschalten.
if (g_vocIndex < 0) return; // Sensor noch stumm
if (!g_fanOn && g_vocIndex >= g_threshold) setFan(true);
if ( g_fanOn && g_vocIndex <= g_threshold - 20) setFan(false);
}
// ── Befehls-Callbacks vom Portal (Kapitel 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(); // sofort anwenden
}
static void onThresholdChanged(float v) {
g_threshold = (int32_t)v;
s_prefs.putInt("thr", g_threshold);
}
// ── setup: Einstellungen, Netz, Sensor, Karte ────────────────
void setup() {
Serial.begin(115200);
// Einstellungen aus NVS (zweite Argumente – Defaults beim ersten Start).
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, Bindung – alles drin
initVocSensor();
// Telemetrie: eigenes Feld vocIndex hinzufügen (Kapitel 5).
s_link.onTelemetryPublish([](JsonObject doc) {
if (g_vocIndex >= 0) {
doc["units"][0]["vocIndex"] = g_vocIndex;
}
});
// Karte: Sensor + Steuerelemente (Kapitel 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 (Kapitel 6, optional).
s_link.card().layoutRow("voc", "fan");
s_link.card().layoutRow("mode", "threshold");
}
// ── loop: Netz immer, Sensor und Logik pro Sekunde ────────
static uint32_t s_lastReadMs = 0;
void loop() {
s_link.loop(); // Netz, Telemetrie, Befehle – immer zuerst
uint32_t now = millis();
if (now - s_lastReadMs >= 1000) {
s_lastReadMs = now;
readVocSensor(); // frischer Messwert…
tickAutoLogic(); // …und sofort Entscheidung
}
}